Add superadmin instance stats card (#2404)

Closes #2401


https://github.com/user-attachments/assets/cbd288d7-8e9c-4e86-ae87-6a308f6bdd58
This commit is contained in:
Emma Segal-Grossman 2025-02-18 17:29:26 -05:00 committed by GitHub
parent f1dc790ab4
commit 905fe059a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 225 additions and 0 deletions

View File

@ -0,0 +1 @@
import "./stats";

View File

@ -0,0 +1,219 @@
import { localized, msg } from "@lit/localize";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { guard } from "lit/directives/guard.js";
import { BtrixElement } from "@/classes/BtrixElement";
import { SubscriptionStatus } from "@/types/billing";
import type { OrgData } from "@/types/org";
export function computeStats(orgData: OrgData[] = []) {
// orgs
const orgs = { all: orgData.length, active: 0 };
// users
const allUsersSet = new Set<string>();
const activeUsersSet = new Set<string>();
// subscriptions
const subscriptions = {
total: 0,
active: 0,
trialing: 0,
trialingCancelled: 0,
pausedPaymentFailed: 0,
cancelled: 0,
};
// storage
const storage = { total: 0, active: 0 };
orgData.forEach((org) => {
Object.keys(org.users ?? {}).forEach((user) => allUsersSet.add(user));
if (!org.readOnly) {
orgs.active++;
Object.keys(org.users ?? {}).forEach((user) => activeUsersSet.add(user));
storage.active += org.bytesStored;
}
if (org.subscription) {
subscriptions.total++;
switch (org.subscription.status) {
case SubscriptionStatus.Active:
subscriptions.active++;
break;
case SubscriptionStatus.Trialing:
subscriptions.trialing++;
break;
case SubscriptionStatus.TrialingCanceled:
subscriptions.trialingCancelled++;
break;
case SubscriptionStatus.PausedPaymentFailed:
subscriptions.pausedPaymentFailed++;
break;
case SubscriptionStatus.Cancelled:
subscriptions.cancelled++;
break;
}
}
storage.total += org.bytesStored;
});
return {
orgs,
users: {
all: allUsersSet.size,
active: activeUsersSet.size,
},
subscriptions,
storage,
};
}
@customElement("btrix-instance-stats")
@localized()
export class Component extends BtrixElement {
@property({ type: Array })
orgList: OrgData[] = [];
render() {
return guard([this.orgList], () => {
const { orgs, users, subscriptions, storage } = computeStats(
this.orgList,
);
return html`<ul
class="mb-4 grid grid-cols-[auto_1fr] items-baseline justify-items-end gap-x-2 p-3 text-xl *:contents md:rounded-lg md:border md:bg-white md:px-8"
>
<li>
<sl-tooltip placement="left">
<span class="font-bold">${this.localize.number(orgs.active)}</span>
<span
slot="content"
class="grid grid-cols-[1fr_auto] gap-x-1 text-right text-neutral-300"
>
${msg("Total orgs")}:
<span class="text-left font-bold text-white"
>${html`${this.localize.number(orgs.all)}`}</span
>
${msg("Inactive orgs")}:
<span class="text-left font-bold text-white"
>${html`${this.localize.number(orgs.all - orgs.active)}`}</span
>
</span>
</sl-tooltip>
<span class="justify-self-start text-xs text-neutral-600">
${msg("Active Orgs")}
<sl-tooltip content=${msg("Orgs that are not read-only")}
><sl-icon class="align-[-2px]" name="info-circle"></sl-icon
></sl-tooltip>
</span>
</li>
<li>
<sl-tooltip placement="left">
<span class="font-bold">${this.localize.number(users.active)}</span>
<span
slot="content"
class="grid grid-cols-[1fr_auto] gap-x-1 text-right text-neutral-300"
>
${msg("Total users")}:
<span class="text-left font-bold text-white"
>${html`${this.localize.number(users.all)}`}</span
>
${msg("Inactive users")}:
<span class="text-left font-bold text-white"
>${html`${this.localize.number(
users.all - users.active,
)}`}</span
>
</span>
</sl-tooltip>
<span class="justify-self-start text-xs text-neutral-600">
${msg("Active Users")}
<sl-tooltip content=${msg("Users in orgs that are not read-only")}
><sl-icon class="align-[-2px]" name="info-circle"></sl-icon
></sl-tooltip>
</span>
</li>
<li>
<sl-tooltip placement="left">
<span class="font-bold"
>${this.localize.number(subscriptions.active)}
</span>
<span
slot="content"
class="grid grid-cols-[1fr_auto] gap-x-1 text-right text-neutral-300"
>
${msg("Active subscriptions")}:
<span class="text-left font-bold text-white"
>${html`${this.localize.number(subscriptions.active)}`}</span
>
${msg("Trialing subscriptions")}:
<span class="text-left font-bold text-white"
>${html`${this.localize.number(subscriptions.trialing)}`}</span
>
${msg("Cancelled trialing subscriptions")}:
<span class="text-left font-bold text-white"
>${html`${this.localize.number(
subscriptions.trialingCancelled,
)}`}</span
>
${msg("Paused (payment failed) subscriptions")}:
<span class="text-left font-bold text-white"
>${html`${this.localize.number(
subscriptions.pausedPaymentFailed,
)}`}</span
>
${msg("Cancelled subscriptions")}:
<span class="text-left font-bold text-white"
>${html`${this.localize.number(subscriptions.cancelled)}`}</span
>
<hr class="col-span-2 -mx-2 my-1 border-neutral-500" />
${msg("Total subscriptions (all states)")}:
<span class="text-left font-bold text-white"
>${html`${this.localize.number(subscriptions.total)}`}</span
>
</span>
</sl-tooltip>
<span class="justify-self-start text-xs text-neutral-600">
${msg("Active Subscriptions")}
<sl-tooltip
content=${msg(
"Orgs with active subscriptions (including with future cancellation dates)",
)}
><sl-icon class="align-[-2px]" name="info-circle"></sl-icon
></sl-tooltip>
</span>
</li>
<li>
<sl-tooltip placement="left">
<span class="text-xl font-bold"
>${this.localize.bytes(storage.total)}
</span>
<span
slot="content"
class="grid grid-cols-[1fr_auto] gap-x-1 text-right text-neutral-300"
>
${msg("Storage in active orgs")}:
<span class="text-left font-bold text-white"
>${html`${this.localize.bytes(storage.active)}`}</span
>
${msg("Storage in inactive orgs")}:
<span class="text-left font-bold text-white"
>${html`${this.localize.bytes(
storage.total - storage.active,
)}`}</span
>
</span>
</sl-tooltip>
<span class="justify-self-start text-xs text-neutral-600">
${msg("Data Stored")}
<sl-tooltip content=${msg("Across all orgs")}
><sl-icon class="align-[-2px]" name="info-circle"></sl-icon
></sl-tooltip>
</span>
</li>
</ul>`;
});
}
}

View File

@ -5,3 +5,5 @@ import "./collections";
import "./crawl-workflows";
import "./org";
import "./qa";
import("./admin");

View File

@ -156,6 +156,9 @@ export class Admin extends BtrixElement {
</section>
</div>
<div class="col-span-3 md:col-span-1">
<btrix-instance-stats
.orgList=${this.orgList ?? []}
></btrix-instance-stats>
<section class="p-3 md:rounded-lg md:border md:bg-white md:p-8">
<h2 class="mb-3 text-lg font-medium">
${msg("Invite User to Org")}