yex
This commit is contained in:
@@ -11,6 +11,10 @@ export class GatewayAdmin {
|
||||
this.modelForm = root.querySelector("#gw-model-form");
|
||||
this.providerSelects = root.querySelectorAll("[data-provider-select]");
|
||||
this.providers = [];
|
||||
this.quotaRulesBody = root.querySelector("#gw-quota-rules");
|
||||
this.quotaForm = root.querySelector("#gw-quota-form");
|
||||
this.quotaCancelEdit = root.querySelector("#gw-quota-cancel-edit");
|
||||
this.quotaRules = [];
|
||||
}
|
||||
|
||||
async start() {
|
||||
@@ -46,6 +50,12 @@ export class GatewayAdmin {
|
||||
this.modelForm.kind.addEventListener("change", () => this.syncKindFields());
|
||||
this.providersBody.addEventListener("click", (event) => this.onProviderClick(event));
|
||||
this.modelsBody.addEventListener("click", (event) => this.onModelClick(event));
|
||||
this.quotaForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
this.saveQuotaRule();
|
||||
});
|
||||
this.quotaRulesBody.addEventListener("click", (event) => this.onQuotaRuleClick(event));
|
||||
this.quotaCancelEdit.addEventListener("click", () => this.resetQuotaForm());
|
||||
}
|
||||
|
||||
notify(message, type) {
|
||||
@@ -57,9 +67,10 @@ export class GatewayAdmin {
|
||||
async reload() {
|
||||
const providerCount = await this.loadProviders();
|
||||
const modelCount = await this.loadModels();
|
||||
const quotaCount = await this.loadQuotaRules();
|
||||
const count = this.root.querySelector("#gw-count");
|
||||
if (count) {
|
||||
count.textContent = `${providerCount} providers, ${modelCount} routes`;
|
||||
count.textContent = `${providerCount} providers, ${modelCount} routes, ${quotaCount} quota rules`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,4 +347,115 @@ export class GatewayAdmin {
|
||||
this.notify(err.message || "Delete failed", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async loadQuotaRules() {
|
||||
const data = await Http.getJson("/admin/gateway/quota-rules");
|
||||
this.quotaRules = data.rules || [];
|
||||
this.renderQuotaDefaults(data.defaults || {});
|
||||
this.renderQuotaRules();
|
||||
return this.quotaRules.length;
|
||||
}
|
||||
|
||||
renderQuotaDefaults(defaults) {
|
||||
const el = this.root.querySelector("#gw-quota-defaults");
|
||||
if (!el) return;
|
||||
const fmt = (v) => (Number(v) > 0 ? `$${Number(v).toFixed(2)}/24h` : "unlimited");
|
||||
el.innerHTML = `
|
||||
<strong>global defaults</strong> (from <a href="/admin/services">Services config</a>, apply per caller with no matching rule):
|
||||
member <code class="gw-code">${fmt(defaults.user)}</code>,
|
||||
admin <code class="gw-code">${fmt(defaults.admin)}</code>,
|
||||
guest <code class="gw-code">${fmt(defaults.guest)}</code>,
|
||||
internal <code class="gw-code">${fmt(defaults.internal)}</code>,
|
||||
access key <code class="gw-code">${fmt(defaults.key)}</code>`;
|
||||
}
|
||||
|
||||
scopeLabel(rule) {
|
||||
const parts = [];
|
||||
parts.push(rule.owner_kind ? `role=${rule.owner_kind}` : "role=any");
|
||||
parts.push(rule.owner_id ? `user=${rule.owner_id}` : "user=any");
|
||||
parts.push(rule.app_reference ? `app=${rule.app_reference}` : "app=any");
|
||||
return parts.join(", ");
|
||||
}
|
||||
|
||||
renderQuotaRules() {
|
||||
if (!this.quotaRules.length) {
|
||||
this.quotaRulesBody.innerHTML = `<tr><td colspan="6" class="admin-empty">No quota rules. Every caller is capped by the global defaults above.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
this.quotaRulesBody.innerHTML = this.quotaRules
|
||||
.map((r) => {
|
||||
const limit = Number(r.limit_usd) > 0 ? `$${Number(r.limit_usd).toFixed(2)}` : "unlimited";
|
||||
const spent = `$${Number(r.spent_24h_usd || 0).toFixed(4)}`;
|
||||
return `<tr>
|
||||
<td><code class="gw-code">${this.escape(this.scopeLabel(r))}</code></td>
|
||||
<td>${limit}</td>
|
||||
<td>${spent}</td>
|
||||
<td>${r.is_active ? "yes" : "no"}</td>
|
||||
<td>${this.escape(r.label || "")}</td>
|
||||
<td class="gw-actions">
|
||||
<button class="admin-btn admin-btn-sm" data-edit-quota='${this.attr(JSON.stringify(r))}'>Edit</button>
|
||||
<button class="admin-btn admin-btn-sm admin-btn-danger" data-del-quota="${this.attr(r.uid)}">Delete</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
async saveQuotaRule() {
|
||||
const values = this.formValues(this.quotaForm);
|
||||
const payload = {
|
||||
owner_kind: values.owner_kind || null,
|
||||
owner_id: values.owner_id || null,
|
||||
app_reference: values.app_reference || null,
|
||||
limit_usd: parseFloat(values.limit_usd) || 0,
|
||||
is_active: values.is_active === "1",
|
||||
label: values.label || "",
|
||||
};
|
||||
if (values.uid) payload.uid = values.uid;
|
||||
try {
|
||||
await Http.postJson("/admin/gateway/quota-rules", payload);
|
||||
this.resetQuotaForm();
|
||||
this.notify("Quota rule saved", "success");
|
||||
await this.reload();
|
||||
} catch (err) {
|
||||
this.notify(err.message || "Save failed", "error");
|
||||
}
|
||||
}
|
||||
|
||||
fillQuotaForm(rule) {
|
||||
const form = this.quotaForm;
|
||||
form.uid.value = rule.uid || "";
|
||||
form.owner_kind.value = rule.owner_kind || "";
|
||||
form.owner_id.value = rule.owner_id || "";
|
||||
form.app_reference.value = rule.app_reference || "";
|
||||
form.limit_usd.value = rule.limit_usd || 0;
|
||||
form.is_active.value = rule.is_active ? "1" : "0";
|
||||
form.label.value = rule.label || "";
|
||||
this.quotaCancelEdit.hidden = false;
|
||||
form.owner_kind.scrollIntoView({ block: "center" });
|
||||
}
|
||||
|
||||
resetQuotaForm() {
|
||||
this.quotaForm.reset();
|
||||
this.quotaForm.uid.value = "";
|
||||
this.quotaCancelEdit.hidden = true;
|
||||
}
|
||||
|
||||
onQuotaRuleClick(event) {
|
||||
const editRaw = event.target.dataset.editQuota;
|
||||
const delUid = event.target.dataset.delQuota;
|
||||
if (editRaw) this.fillQuotaForm(JSON.parse(editRaw));
|
||||
if (delUid) this.deleteQuotaRule(delUid);
|
||||
}
|
||||
|
||||
async deleteQuotaRule(uid) {
|
||||
if (!(await this.confirmDelete("Delete this quota rule?"))) return;
|
||||
try {
|
||||
await this.remove(`/admin/gateway/quota-rules/${encodeURIComponent(uid)}`);
|
||||
this.notify("Quota rule deleted", "success");
|
||||
await this.reload();
|
||||
} catch (err) {
|
||||
this.notify(err.message || "Delete failed", "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user