Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add superadmin instance stats card #2404

Merged
merged 1 commit into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/features/admin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "./stats";
219 changes: 219 additions & 0 deletions frontend/src/features/admin/stats.ts
Original file line number Diff line number Diff line change
@@ -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>`;
});
}
}
2 changes: 2 additions & 0 deletions frontend/src/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ import "./collections";
import "./crawl-workflows";
import "./org";
import "./qa";

import("./admin");
3 changes: 3 additions & 0 deletions frontend/src/pages/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")}
Expand Down
Loading