From 87c99e64cd684699b52684c9f53f635685099bc5 Mon Sep 17 00:00:00 2001 From: lethdat Date: Tue, 17 Mar 2026 22:49:58 +0700 Subject: [PATCH] feat: add AsyncSelect component and update related types and headers handling --- components.d.ts | 2 + public/locales/en/translation.json | 2 +- src/api/httpClientAdapter.client.ts | 18 ++++-- src/api/httpClientAdapter.server.ts | 22 ++++--- src/api/rpcclient.ts | 1 + src/components/ui/AsyncSelect.vue | 84 +++++++++++++++++++++++++++ src/routes/admin/AdTemplates.vue | 40 +++++++------ src/routes/admin/Agents.vue | 25 ++------ src/routes/admin/Logs.vue | 12 +--- src/routes/admin/Payments.vue | 40 +++++++------ src/routes/admin/Users.vue | 19 ++++-- src/routes/admin/Videos.vue | 40 +++++++------ src/server/middlewares/setup.ts | 1 + src/server/routes/rpc/index.ts | 31 +++++++--- src/server/routes/ssr.ts | 9 ++- src/shared/secure-json-transformer.ts | 44 ++++++++------ src/type.d.ts | 6 +- 17 files changed, 257 insertions(+), 139 deletions(-) create mode 100644 src/components/ui/AsyncSelect.vue diff --git a/components.d.ts b/components.d.ts index 7970f8d..6f13e16 100644 --- a/components.d.ts +++ b/components.d.ts @@ -27,6 +27,7 @@ declare module 'vue' { AppTopLoadingBar: typeof import('./src/components/AppTopLoadingBar.vue')['default'] ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default'] ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default'] + AsyncSelect: typeof import('./src/components/ui/AsyncSelect.vue')['default'] BaseTable: typeof import('./src/components/ui/table/BaseTable.vue')['default'] Bell: typeof import('./src/components/icons/Bell.vue')['default'] BellIcon: typeof import('./src/components/icons/BellIcon.vue')['default'] @@ -108,6 +109,7 @@ declare global { const AppTopLoadingBar: typeof import('./src/components/AppTopLoadingBar.vue')['default'] const ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default'] const ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default'] + const AsyncSelect: typeof import('./src/components/ui/AsyncSelect.vue')['default'] const BaseTable: typeof import('./src/components/ui/table/BaseTable.vue')['default'] const Bell: typeof import('./src/components/icons/Bell.vue')['default'] const BellIcon: typeof import('./src/components/icons/BellIcon.vue')['default'] diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index d200e56..2a69b33 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -716,7 +716,7 @@ }, "filters": { "searchPlaceholder": "Search videos...", - "rangeOfTotal": "{first}–{last} of {{total}}", + "rangeOfTotal": "{{first}}–{{last}} of {{total}}", "previousPageAria": "Previous page", "nextPageAria": "Next page", "allStatus": "All Status", diff --git a/src/api/httpClientAdapter.client.ts b/src/api/httpClientAdapter.client.ts index bba1e02..7a252c0 100644 --- a/src/api/httpClientAdapter.client.ts +++ b/src/api/httpClientAdapter.client.ts @@ -11,20 +11,23 @@ export function httpClientAdapter(opts: { }): TinyRpcClientAdapter { const JSON: JsonTransformer = { parse: globalThis.JSON.parse, - stringify: globalThis.JSON.stringify, + stringify: globalThis.JSON.stringify as JsonTransformer["stringify"], ...opts.JSON, }; return { send: async (data) => { const url = [opts.url, data.path].join("/"); - const payload = JSON.stringify(data.args); - console.log("RPC Request:", payload); + const extraHeaders = opts.headers ? await opts.headers() : {}; + const payload = JSON.stringify(data.args, (headerObj) => { + if (headerObj) { + Object.assign(extraHeaders, headerObj); + } + }); + const method = opts.pathsForGET?.includes(data.path) ? "GET" : "POST"; - const extraHeaders = opts.headers ? await opts.headers() : {}; - let req: Request; if (method === "GET") { req = new Request( @@ -47,6 +50,7 @@ export function httpClientAdapter(opts: { }); } let res: Response; + res = await fetch(req); if (!res.ok) { // throw new Error(`HTTP error: ${res.status}`); @@ -60,8 +64,10 @@ export function httpClientAdapter(opts: { ); // throw TinyRpcError.deserialize(res.status); } + const result: Result = JSON.parse( - await res.text() + await res.text(), + () => Object.fromEntries((res.headers as any).entries() ?? []) ); if (!result.ok) { throw TinyRpcError.deserialize(result.value); diff --git a/src/api/httpClientAdapter.server.ts b/src/api/httpClientAdapter.server.ts index 1a98ead..0acf2cd 100644 --- a/src/api/httpClientAdapter.server.ts +++ b/src/api/httpClientAdapter.server.ts @@ -13,14 +13,18 @@ export function httpClientAdapter(opts: { }): TinyRpcClientAdapter { const JSON: JsonTransformer = { parse: globalThis.JSON.parse, - stringify: globalThis.JSON.stringify, + stringify: globalThis.JSON.stringify as JsonTransformer["stringify"], ...opts.JSON, }; return { send: async (data) => { const url = [opts.url, data.path].join("/"); - const payload = JSON.stringify(data.args); - console.log("RPC Request:", payload); + const extraHeaders = opts.headers ? await opts.headers() : {}; + const payload = JSON.stringify(data.args, (headerObj) => { + if (headerObj) { + Object.assign(extraHeaders, headerObj); + } + }); const method = opts.pathsForGET?.includes(data.path) ? "GET" : "POST"; @@ -29,7 +33,10 @@ export function httpClientAdapter(opts: { req = new Request( url + "?" + - new URLSearchParams({ [GET_PAYLOAD_PARAM]: payload }) + new URLSearchParams({ [GET_PAYLOAD_PARAM]: payload }), + { + headers: extraHeaders + } ); } else { req = new Request(url, { @@ -37,6 +44,7 @@ export function httpClientAdapter(opts: { body: payload, headers: { "content-type": "application/json; charset=utf-8", + ...extraHeaders, }, credentials: "include", }); @@ -67,11 +75,9 @@ export function httpClientAdapter(opts: { ); // throw TinyRpcError.deserialize(res.status); } - // if (res.headers.get("set-cookie")) { - // console.log("Response has set-cookie header:", res.headers.get("set-cookie")); - // } const result: Result = JSON.parse( - await res.text() + await res.text(), + () => Object.fromEntries((res.headers as any).entries() ?? []) ); if (!result.ok) { throw TinyRpcError.deserialize(result.value); diff --git a/src/api/rpcclient.ts b/src/api/rpcclient.ts index 8f03be9..2004f86 100644 --- a/src/api/rpcclient.ts +++ b/src/api/rpcclient.ts @@ -27,6 +27,7 @@ export const client = proxyTinyRpc({ url: `${url}${targetEndpoint}`, pathsForGET: ["health"], JSON: clientJSON, + headers: () => Promise.resolve({}) }).send(data); }, }, diff --git a/src/components/ui/AsyncSelect.vue b/src/components/ui/AsyncSelect.vue new file mode 100644 index 0000000..ed27523 --- /dev/null +++ b/src/components/ui/AsyncSelect.vue @@ -0,0 +1,84 @@ + + + \ No newline at end of file diff --git a/src/routes/admin/AdTemplates.vue b/src/routes/admin/AdTemplates.vue index e9ce070..1f8f8bb 100644 --- a/src/routes/admin/AdTemplates.vue +++ b/src/routes/admin/AdTemplates.vue @@ -310,24 +310,24 @@ const columns = computed[]>(() => [ }, ]); -useAdminPageHeader(() => ({ - eyebrow: "Advertising", - badge: `${total.value} total templates`, - actions: [ - { - label: "Refresh", - variant: "secondary", - onClick: loadTemplates, - }, - { - label: "Create template", - onClick: () => { - actionError.value = null; - createOpen.value = true; - }, - }, - ], -})); +// useAdminPageHeader(() => ({ +// eyebrow: "Advertising", +// badge: `${total.value} total templates`, +// actions: [ +// { +// label: "Refresh", +// variant: "secondary", +// onClick: loadTemplates, +// }, +// { +// label: "Create template", +// onClick: () => { +// actionError.value = null; +// createOpen.value = true; +// }, +// }, +// ], +// })); onMounted(loadTemplates); @@ -363,6 +363,10 @@ onMounted(loadTemplates);
{{ error }}
+ >; type AdminAgentRow = NonNullable[number]; @@ -144,25 +143,6 @@ const statusBadgeClass = (status?: string) => { return "border-border bg-muted/40 text-foreground/70"; }; -useAdminPageHeader(() => ({ - eyebrow: "Workers", - badge: `${rows.value.length} agents connected`, - actions: [{ - label: "Refresh agents", - variant: "secondary", - onClick: loadAgents, - }], -})); - -useAdminPageHeader(() => ({ - eyebrow: "Workers", - badge: `${rows.value.length} agents connected`, - actions: [{ - label: "Refresh agents", - variant: "secondary", - onClick: loadAgents, - }], -})); const columns = computed[]>(() => [ { @@ -294,7 +274,10 @@ onMounted(loadAgents);
{{ error }}
- + + import { client as rpcClient } from "@/api/rpcclient"; -import { useAdminRuntimeMqtt } from "@/composables/useAdminRuntimeMqtt"; import AppButton from "@/components/app/AppButton.vue"; import AppInput from "@/components/app/AppInput.vue"; +import { useAdminRuntimeMqtt } from "@/composables/useAdminRuntimeMqtt"; import SettingsSectionCard from "@/routes/settings/components/SettingsSectionCard.vue"; import { computed, ref } from "vue"; import AdminSectionShell from "./components/AdminSectionShell.vue"; -import { useAdminPageHeader } from "./components/useAdminPageHeader"; const loading = ref(false); const error = ref(null); @@ -60,15 +59,6 @@ useAdminRuntimeMqtt(({ topic, payload }) => { } }); -useAdminPageHeader(() => ({ - eyebrow: "Observability", - badge: activeJobId.value ? "Live tail attached" : "Awaiting job selection", - actions: [{ - label: "Load logs", - variant: "secondary", - onClick: loadLogs, - }], -}));