import { type EnvironmentId, UsageLimitSourceId } from "@t3tools/contracts";import { useState } from "react"; import { useUpdateEnvironmentSettings } from "../../hooks/useSettings";import { Button } from "../ui/button";import { Dialog, DialogDescription, DialogFooter, DialogHeader, DialogPanel, DialogPopup, DialogTitle,} from "../ui/dialog";import { Input } from "../ui/input";import { Label } from "../ui/label"; /** * Stable per hub and readable in settings.json. Dots and dashes in the host * are kept so `foo-bar.com` and `foo.bar.com` do not collide; anything else * (a port's colon, a path) is folded to a dash. */function sourceIdFromUrl(url: string): UsageLimitSourceId { let host = url; try { host = new URL(url).host; } catch { // Keep the raw text; the server reports the bad URL on its row. } const slug = host .toLowerCase() .replace(/[^a-z0-9.-]+/g, "-") .replace(/^-+|-+$/g, ""); return UsageLimitSourceId.make(`cliproxy-${slug || "hub"}`);} /** * Adds a CLIProxyAPI hub from provider settings on one environment. The * management key is sent once and kept in that server's secret store; * settings only ever carry a redaction marker for it afterwards. */export function AddUsageLimitSourceDialog({ open, onOpenChange, environmentId, environmentLabel,}: { readonly open: boolean; readonly onOpenChange: (open: boolean) => void; readonly environmentId: EnvironmentId; readonly environmentLabel: string;}) { const updateSettings = useUpdateEnvironmentSettings(environmentId); const [label, setLabel] = useState(""); const [url, setUrl] = useState(""); const [managementKey, setManagementKey] = useState(""); const trimmedUrl = url.trim(); const canSave = trimmedUrl.length > 0 && managementKey.trim().length > 0; const reset = () => { setLabel(""); setUrl(""); setManagementKey(""); }; const save = () => { if (!canSave) return; const id = sourceIdFromUrl(trimmedUrl); // The patch names only this entry; the server merges it into its map. updateSettings({ usageLimitSources: { [id]: { kind: "cliproxy", ...(label.trim() ? { label: label.trim() } : {}), url: trimmedUrl, managementKey: managementKey.trim(), enabled: true, }, }, }); reset(); onOpenChange(false); }; return ( <Dialog open={open} onOpenChange={(next) => { if (!next) reset(); onOpenChange(next); }} > <DialogPopup className="max-w-md"> <DialogHeader> <DialogTitle>Add a CLIProxyAPI hub</DialogTitle> <DialogDescription> Show the quota of every account the hub pools, next to the providers on{" "} {environmentLabel}. The key stays on that server. </DialogDescription> </DialogHeader> <DialogPanel> <form className="grid gap-4" onSubmit={(event) => { event.preventDefault(); save(); }} > <div className="grid gap-1.5"> <Label htmlFor="usage-source-url">Hub URL</Label> <Input id="usage-source-url" placeholder="https://hub.example.ts.net:8318" value={url} onChange={(event) => setUrl(event.target.value)} autoFocus /> </div> <div className="grid gap-1.5"> <Label htmlFor="usage-source-key">Management key</Label> <Input id="usage-source-key" type="password" autoComplete="off" value={managementKey} onChange={(event) => setManagementKey(event.target.value)} /> </div> <div className="grid gap-1.5"> <Label htmlFor="usage-source-label">Label (optional)</Label> <Input id="usage-source-label" placeholder="Defaults to the hub's host name" value={label} onChange={(event) => setLabel(event.target.value)} /> </div> </form> </DialogPanel> <DialogFooter variant="bare"> <Button variant="outline" onClick={() => { reset(); onOpenChange(false); }} > Cancel </Button> <Button onClick={save} disabled={!canSave}> Add hub </Button> </DialogFooter> </DialogPopup> </Dialog> );}
import { type EnvironmentId, UsageLimitSourceId } from "@t3tools/contracts";
import { useState } from "react";
import { useUpdateEnvironmentSettings } from "../../hooks/useSettings";
import { Button } from "../ui/button";
import {
Dialog,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
} from "../ui/dialog";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
/**
* Stable per hub and readable in settings.json. Dots and dashes in the host
* are kept so `foo-bar.com` and `foo.bar.com` do not collide; anything else
* (a port's colon, a path) is folded to a dash.
*/
function sourceIdFromUrl(url: string): UsageLimitSourceId {
let host = url;
try {
host = new URL(url).host;
} catch {
// Keep the raw text; the server reports the bad URL on its row.
}
const slug = host
.toLowerCase()
.replace(/[^a-z0-9.-]+/g, "-")
.replace(/^-+|-+$/g, "");
return UsageLimitSourceId.make(`cliproxy-${slug || "hub"}`);
}
/**
* Adds a CLIProxyAPI hub from provider settings on one environment. The
* management key is sent once and kept in that server's secret store;
* settings only ever carry a redaction marker for it afterwards.
*/
export function AddUsageLimitSourceDialog({
open,
onOpenChange,
environmentId,
environmentLabel,
}: {
readonly open: boolean;
readonly onOpenChange: (open: boolean) => void;
readonly environmentId: EnvironmentId;
readonly environmentLabel: string;
}) {
const updateSettings = useUpdateEnvironmentSettings(environmentId);
const [label, setLabel] = useState("");
const [url, setUrl] = useState("");
const [managementKey, setManagementKey] = useState("");
const trimmedUrl = url.trim();
const canSave = trimmedUrl.length > 0 && managementKey.trim().length > 0;
const reset = () => {
setLabel("");
setUrl("");
setManagementKey("");
};
const save = () => {
if (!canSave) return;
const id = sourceIdFromUrl(trimmedUrl);
// The patch names only this entry; the server merges it into its map.
updateSettings({
usageLimitSources: {
[id]: {
kind: "cliproxy",
...(label.trim() ? { label: label.trim() } : {}),
url: trimmedUrl,
managementKey: managementKey.trim(),
enabled: true,
},
},
});
reset();
onOpenChange(false);
};
return (
<Dialog
open={open}
onOpenChange={(next) => {
if (!next) reset();
onOpenChange(next);
}}
>
<DialogPopup className="max-w-md">
<DialogHeader>
<DialogTitle>Add a CLIProxyAPI hub</DialogTitle>
<DialogDescription>
Show the quota of every account the hub pools, next to the providers on{" "}
{environmentLabel}. The key stays on that server.
</DialogDescription>
</DialogHeader>
<DialogPanel>
<form
className="grid gap-4"
onSubmit={(event) => {
event.preventDefault();
save();
}}
>
<div className="grid gap-1.5">
<Label htmlFor="usage-source-url">Hub URL</Label>
<Input
id="usage-source-url"
placeholder="https://hub.example.ts.net:8318"
value={url}
onChange={(event) => setUrl(event.target.value)}
autoFocus
/>
</div>
<div className="grid gap-1.5">
<Label htmlFor="usage-source-key">Management key</Label>
<Input
id="usage-source-key"
type="password"
autoComplete="off"
value={managementKey}
onChange={(event) => setManagementKey(event.target.value)}
/>
</div>
<div className="grid gap-1.5">
<Label htmlFor="usage-source-label">Label (optional)</Label>
<Input
id="usage-source-label"
placeholder="Defaults to the hub's host name"
value={label}
onChange={(event) => setLabel(event.target.value)}
/>
</div>
</form>
</DialogPanel>
<DialogFooter variant="bare">
<Button
variant="outline"
onClick={() => {
reset();
onOpenChange(false);
}}
>
Cancel
</Button>
<Button onClick={save} disabled={!canSave}>
Add hub
</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
);
}