@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,218 @@
|
|
|
1
|
+
import type { Bundle, ChannelRow, ReleaseRow } from "@hot-updater/plugin-core";
|
|
2
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
3
|
+
|
|
4
|
+
import { parseActiveInstallationInput } from "./analytics-input";
|
|
5
|
+
import {
|
|
6
|
+
type AnalyticsOverview,
|
|
7
|
+
createAnalyticsOverviewFromCounts,
|
|
8
|
+
} from "./analytics-overview";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ANALYTICS_PAGE_SIZE = 100;
|
|
11
|
+
|
|
12
|
+
export type AnalyticsCapabilities = {
|
|
13
|
+
readonly capabilities:
|
|
14
|
+
| { readonly analytics: false }
|
|
15
|
+
| {
|
|
16
|
+
readonly analytics: true;
|
|
17
|
+
readonly mode: "bounded";
|
|
18
|
+
readonly maxMatchingRows: number;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type BundlePage = {
|
|
23
|
+
readonly data: readonly Bundle[];
|
|
24
|
+
readonly pagination: {
|
|
25
|
+
readonly hasNextPage: boolean;
|
|
26
|
+
readonly currentPage: number;
|
|
27
|
+
readonly totalPages: number;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type AnalyticsOverviewDependencies = {
|
|
32
|
+
readonly runtime: unknown;
|
|
33
|
+
readonly getBundles: (options: {
|
|
34
|
+
readonly limit: number;
|
|
35
|
+
readonly page: number;
|
|
36
|
+
}) => Promise<BundlePage>;
|
|
37
|
+
readonly getReleases: (options: {
|
|
38
|
+
readonly beforeReleaseId?: string;
|
|
39
|
+
readonly limit: number;
|
|
40
|
+
}) => Promise<readonly ReleaseRow[]>;
|
|
41
|
+
readonly getChannels: () => Promise<readonly ChannelRow[]>;
|
|
42
|
+
readonly pageSize?: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export class AnalyticsNotSupportedError extends Error {
|
|
46
|
+
constructor() {
|
|
47
|
+
super("Analytics are not supported by the configured database plugin.");
|
|
48
|
+
this.name = "AnalyticsNotSupportedError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class AnalyticsBundlePaginationError extends Error {
|
|
53
|
+
readonly name = "AnalyticsBundlePaginationError";
|
|
54
|
+
|
|
55
|
+
constructor(
|
|
56
|
+
readonly requestedPage: number,
|
|
57
|
+
readonly reason: string,
|
|
58
|
+
) {
|
|
59
|
+
super(`Invalid bundle pagination for page ${requestedPage}: ${reason}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const getAnalyticsCapabilities = async (
|
|
64
|
+
runtime: unknown,
|
|
65
|
+
): Promise<AnalyticsCapabilities> => {
|
|
66
|
+
const { getAnalyticsCapability } = await import("./server/runtime.server");
|
|
67
|
+
const capability = await getAnalyticsCapability(runtime);
|
|
68
|
+
if (!capability.analytics || !capability.analyticsQueries) {
|
|
69
|
+
return { capabilities: { analytics: false } };
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
capabilities: {
|
|
73
|
+
analytics: true,
|
|
74
|
+
mode: "bounded",
|
|
75
|
+
maxMatchingRows: capability.maxMatchingRows,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const collectBundles = async (
|
|
81
|
+
getBundles: AnalyticsOverviewDependencies["getBundles"],
|
|
82
|
+
pageSize: number,
|
|
83
|
+
): Promise<readonly Bundle[]> => {
|
|
84
|
+
const bundles: Bundle[] = [];
|
|
85
|
+
let page = 1;
|
|
86
|
+
while (true) {
|
|
87
|
+
const result = await getBundles({ limit: pageSize, page });
|
|
88
|
+
const { currentPage, hasNextPage, totalPages } = result.pagination;
|
|
89
|
+
const isEmptyFirstPage =
|
|
90
|
+
page === 1 &&
|
|
91
|
+
currentPage === 1 &&
|
|
92
|
+
totalPages === 0 &&
|
|
93
|
+
result.data.length === 0 &&
|
|
94
|
+
hasNextPage === false;
|
|
95
|
+
|
|
96
|
+
if (currentPage !== page) {
|
|
97
|
+
throw new AnalyticsBundlePaginationError(
|
|
98
|
+
page,
|
|
99
|
+
`currentPage must equal ${page}, received ${currentPage}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (!Number.isFinite(totalPages) || !Number.isInteger(totalPages)) {
|
|
103
|
+
throw new AnalyticsBundlePaginationError(
|
|
104
|
+
page,
|
|
105
|
+
`totalPages must be a finite integer, received ${totalPages}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (totalPages < 0) {
|
|
109
|
+
throw new AnalyticsBundlePaginationError(
|
|
110
|
+
page,
|
|
111
|
+
`totalPages must be nonnegative, received ${totalPages}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (!isEmptyFirstPage && totalPages < currentPage) {
|
|
115
|
+
throw new AnalyticsBundlePaginationError(
|
|
116
|
+
page,
|
|
117
|
+
`totalPages ${totalPages} is lower than currentPage ${currentPage}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (!isEmptyFirstPage && hasNextPage !== currentPage < totalPages) {
|
|
121
|
+
throw new AnalyticsBundlePaginationError(
|
|
122
|
+
page,
|
|
123
|
+
"hasNextPage contradicts currentPage and totalPages",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
bundles.push(...result.data);
|
|
127
|
+
if (!hasNextPage) {
|
|
128
|
+
return bundles;
|
|
129
|
+
}
|
|
130
|
+
page += 1;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const collectReleases = async (
|
|
135
|
+
getReleases: AnalyticsOverviewDependencies["getReleases"],
|
|
136
|
+
pageSize: number,
|
|
137
|
+
): Promise<readonly ReleaseRow[]> => {
|
|
138
|
+
const releases: ReleaseRow[] = [];
|
|
139
|
+
const cursors = new Set<string>();
|
|
140
|
+
let beforeReleaseId: string | undefined;
|
|
141
|
+
while (true) {
|
|
142
|
+
const page = await getReleases({ beforeReleaseId, limit: pageSize });
|
|
143
|
+
releases.push(...page);
|
|
144
|
+
if (page.length < pageSize) return releases;
|
|
145
|
+
const nextCursor = page.at(-1)?.id;
|
|
146
|
+
if (!nextCursor || cursors.has(nextCursor)) {
|
|
147
|
+
throw new AnalyticsBundlePaginationError(
|
|
148
|
+
releases.length,
|
|
149
|
+
"release cursor did not advance",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
cursors.add(nextCursor);
|
|
153
|
+
beforeReleaseId = nextCursor;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const collectAnalyticsOverview = async ({
|
|
158
|
+
runtime,
|
|
159
|
+
getBundles,
|
|
160
|
+
getReleases,
|
|
161
|
+
getChannels,
|
|
162
|
+
pageSize = DEFAULT_ANALYTICS_PAGE_SIZE,
|
|
163
|
+
}: AnalyticsOverviewDependencies): Promise<AnalyticsOverview> => {
|
|
164
|
+
const { capabilities } = await getAnalyticsCapabilities(runtime);
|
|
165
|
+
if (!capabilities.analytics) {
|
|
166
|
+
throw new AnalyticsNotSupportedError();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const [bundles, releases, channels] = await Promise.all([
|
|
170
|
+
collectBundles(getBundles, pageSize),
|
|
171
|
+
collectReleases(getReleases, pageSize),
|
|
172
|
+
getChannels(),
|
|
173
|
+
]);
|
|
174
|
+
const overview = await (
|
|
175
|
+
runtime as import("@hot-updater/server").AnalyticsProvider
|
|
176
|
+
).getBundleEventOverview();
|
|
177
|
+
return createAnalyticsOverviewFromCounts(
|
|
178
|
+
bundles,
|
|
179
|
+
releases,
|
|
180
|
+
channels,
|
|
181
|
+
overview.trackedInstallations,
|
|
182
|
+
overview.bundles,
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export const getAnalyticsCapabilitiesRpc = createServerFn({
|
|
187
|
+
method: "GET",
|
|
188
|
+
}).handler(async () => {
|
|
189
|
+
const { prepareConfig } = await import("./server/config.server");
|
|
190
|
+
const { hotUpdater } = await prepareConfig();
|
|
191
|
+
return getAnalyticsCapabilities(hotUpdater);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
export const getAnalyticsOverviewRpc = createServerFn({
|
|
195
|
+
method: "GET",
|
|
196
|
+
}).handler(async () => {
|
|
197
|
+
const { prepareConfig } = await import("./server/config.server");
|
|
198
|
+
const { config, databaseClient, hotUpdater } = await prepareConfig();
|
|
199
|
+
return collectAnalyticsOverview({
|
|
200
|
+
runtime: hotUpdater,
|
|
201
|
+
getBundles: (options) => databaseClient.getBundles(options),
|
|
202
|
+
getReleases: (options) => config.database.models.releases.findMany(options),
|
|
203
|
+
getChannels: () =>
|
|
204
|
+
config.database.models.channels.list({}).then(({ channels }) => channels),
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
export const getActiveInstallationOverviewRpc = createServerFn({
|
|
209
|
+
method: "GET",
|
|
210
|
+
})
|
|
211
|
+
.validator(parseActiveInstallationInput)
|
|
212
|
+
.handler(async ({ data }) => {
|
|
213
|
+
const { prepareConfig } = await import("./server/config.server");
|
|
214
|
+
const { getActiveInstallationOverview } =
|
|
215
|
+
await import("./server/runtime.server");
|
|
216
|
+
const { hotUpdater } = await prepareConfig();
|
|
217
|
+
return getActiveInstallationOverview(hotUpdater, data);
|
|
218
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { redirect } from "@tanstack/react-router";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createApiKeyRpc,
|
|
7
|
+
getApiKeyCapabilityRpc,
|
|
8
|
+
listApiKeysRpc,
|
|
9
|
+
revokeApiKeyRpc,
|
|
10
|
+
} from "./api-keys-rpc";
|
|
11
|
+
|
|
12
|
+
export const apiKeyQueryKeys = {
|
|
13
|
+
capability: ["api-keys", "capability"] as const,
|
|
14
|
+
list: ["api-keys", "list"] as const,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const getApiKeyCapabilityQueryOptions = () => ({
|
|
18
|
+
queryKey: apiKeyQueryKeys.capability,
|
|
19
|
+
queryFn: () => getApiKeyCapabilityRpc(),
|
|
20
|
+
staleTime: Infinity,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const useApiKeyCapabilityQuery = () =>
|
|
24
|
+
useQuery(getApiKeyCapabilityQueryOptions());
|
|
25
|
+
|
|
26
|
+
export const ensureApiKeyRouteAccess = async (
|
|
27
|
+
queryClient: QueryClient,
|
|
28
|
+
): Promise<void> => {
|
|
29
|
+
const capability = await queryClient.ensureQueryData(
|
|
30
|
+
getApiKeyCapabilityQueryOptions(),
|
|
31
|
+
);
|
|
32
|
+
if (!capability.apiKeys) {
|
|
33
|
+
throw redirect({
|
|
34
|
+
to: "/",
|
|
35
|
+
search: {},
|
|
36
|
+
replace: true,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const useApiKeysQuery = () =>
|
|
42
|
+
useQuery({
|
|
43
|
+
queryKey: apiKeyQueryKeys.list,
|
|
44
|
+
queryFn: () => listApiKeysRpc(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const useCreateApiKeyMutation = () => {
|
|
48
|
+
const queryClient = useQueryClient();
|
|
49
|
+
return useMutation({
|
|
50
|
+
mutationFn: (name: string) => createApiKeyRpc({ data: { name } }),
|
|
51
|
+
onSuccess: async () => {
|
|
52
|
+
await queryClient.invalidateQueries({
|
|
53
|
+
queryKey: apiKeyQueryKeys.list,
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const useRevokeApiKeyMutation = () => {
|
|
60
|
+
const queryClient = useQueryClient();
|
|
61
|
+
return useMutation({
|
|
62
|
+
mutationFn: (id: string) => revokeApiKeyRpc({ data: { id } }),
|
|
63
|
+
onSuccess: async () => {
|
|
64
|
+
await queryClient.invalidateQueries({
|
|
65
|
+
queryKey: apiKeyQueryKeys.list,
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ApiKeyRow } from "@hot-updater/plugin-core";
|
|
2
|
+
import { createApiKey } from "@hot-updater/server";
|
|
3
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
4
|
+
|
|
5
|
+
export type ApiKeyView = Omit<ApiKeyRow, "hash">;
|
|
6
|
+
|
|
7
|
+
export const toApiKeyView = ({
|
|
8
|
+
hash: _hash,
|
|
9
|
+
...record
|
|
10
|
+
}: ApiKeyRow): ApiKeyView => record;
|
|
11
|
+
|
|
12
|
+
const parseName = (input: unknown): { readonly name: string } => {
|
|
13
|
+
const name =
|
|
14
|
+
typeof input === "object" && input !== null
|
|
15
|
+
? Reflect.get(input, "name")
|
|
16
|
+
: undefined;
|
|
17
|
+
if (typeof name !== "string") {
|
|
18
|
+
throw new TypeError("API key name must be a string.");
|
|
19
|
+
}
|
|
20
|
+
return { name };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const parseId = (input: unknown): { readonly id: string } => {
|
|
24
|
+
const id =
|
|
25
|
+
typeof input === "object" && input !== null
|
|
26
|
+
? Reflect.get(input, "id")
|
|
27
|
+
: undefined;
|
|
28
|
+
if (typeof id !== "string" || !/^api-[A-Za-z0-9_-]{43}$/u.test(id)) {
|
|
29
|
+
throw new TypeError("Invalid API key id.");
|
|
30
|
+
}
|
|
31
|
+
return { id };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const requireStore = async () => {
|
|
35
|
+
const { prepareConfig } = await import("./server/config.server");
|
|
36
|
+
const { apiKeyStore } = await prepareConfig();
|
|
37
|
+
if (apiKeyStore === null) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
"API keys are not supported by the configured database plugin.",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return apiKeyStore;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const getApiKeyCapabilityRpc = createServerFn({
|
|
46
|
+
method: "GET",
|
|
47
|
+
}).handler(async () => {
|
|
48
|
+
const { prepareConfig } = await import("./server/config.server");
|
|
49
|
+
const { apiKeyStore } = await prepareConfig();
|
|
50
|
+
return { apiKeys: apiKeyStore !== null } as const;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const listApiKeysRpc = createServerFn({
|
|
54
|
+
method: "GET",
|
|
55
|
+
}).handler(async () => {
|
|
56
|
+
const store = await requireStore();
|
|
57
|
+
const records = await store.list();
|
|
58
|
+
return [...records]
|
|
59
|
+
.sort((left, right) => right.created_at_ms - left.created_at_ms)
|
|
60
|
+
.map(toApiKeyView);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const createApiKeyRpc = createServerFn({ method: "POST" })
|
|
64
|
+
.validator(parseName)
|
|
65
|
+
.handler(async ({ data }) => {
|
|
66
|
+
const store = await requireStore();
|
|
67
|
+
const created = await createApiKey({
|
|
68
|
+
apiKeys: store,
|
|
69
|
+
name: data.name,
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
apiKey: created.apiKey,
|
|
73
|
+
record: created.record,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const revokeApiKeyRpc = createServerFn({ method: "POST" })
|
|
78
|
+
.validator(parseId)
|
|
79
|
+
.handler(async ({ data }) => {
|
|
80
|
+
const store = await requireStore();
|
|
81
|
+
const revoked = await store.revoke({
|
|
82
|
+
id: data.id,
|
|
83
|
+
revokedAtMs: Date.now(),
|
|
84
|
+
});
|
|
85
|
+
if (revoked === null) throw new Error("API key not found.");
|
|
86
|
+
return toApiKeyView(revoked);
|
|
87
|
+
});
|