From 905fe059a4c232c4adbcabdb0376864a3df76ff2 Mon Sep 17 00:00:00 2001 From: Emma Segal-Grossman Date: Tue, 18 Feb 2025 17:29:26 -0500 Subject: [PATCH] Add superadmin instance stats card (#2404) Closes #2401 https://github.com/user-attachments/assets/cbd288d7-8e9c-4e86-ae87-6a308f6bdd58 --- frontend/src/features/admin/index.ts | 1 + frontend/src/features/admin/stats.ts | 219 +++++++++++++++++++++++++++ frontend/src/features/index.ts | 2 + frontend/src/pages/admin.ts | 3 + 4 files changed, 225 insertions(+) create mode 100644 frontend/src/features/admin/index.ts create mode 100644 frontend/src/features/admin/stats.ts diff --git a/frontend/src/features/admin/index.ts b/frontend/src/features/admin/index.ts new file mode 100644 index 00000000..526b8901 --- /dev/null +++ b/frontend/src/features/admin/index.ts @@ -0,0 +1 @@ +import "./stats"; diff --git a/frontend/src/features/admin/stats.ts b/frontend/src/features/admin/stats.ts new file mode 100644 index 00000000..b950cbf2 --- /dev/null +++ b/frontend/src/features/admin/stats.ts @@ -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(); + const activeUsersSet = new Set(); + + // 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`
    +
  • + + ${this.localize.number(orgs.active)} + + ${msg("Total orgs")}: + ${html`${this.localize.number(orgs.all)}`} + ${msg("Inactive orgs")}: + ${html`${this.localize.number(orgs.all - orgs.active)}`} + + + + ${msg("Active Orgs")} + + +
  • +
  • + + ${this.localize.number(users.active)} + + ${msg("Total users")}: + ${html`${this.localize.number(users.all)}`} + ${msg("Inactive users")}: + ${html`${this.localize.number( + users.all - users.active, + )}`} + + + + ${msg("Active Users")} + + +
  • +
  • + + ${this.localize.number(subscriptions.active)} + + + ${msg("Active subscriptions")}: + ${html`${this.localize.number(subscriptions.active)}`} + ${msg("Trialing subscriptions")}: + ${html`${this.localize.number(subscriptions.trialing)}`} + ${msg("Cancelled trialing subscriptions")}: + ${html`${this.localize.number( + subscriptions.trialingCancelled, + )}`} + ${msg("Paused (payment failed) subscriptions")}: + ${html`${this.localize.number( + subscriptions.pausedPaymentFailed, + )}`} + ${msg("Cancelled subscriptions")}: + ${html`${this.localize.number(subscriptions.cancelled)}`} +
    + ${msg("Total subscriptions (all states)")}: + ${html`${this.localize.number(subscriptions.total)}`} +
    +
    + + ${msg("Active Subscriptions")} + + +
  • +
  • + + ${this.localize.bytes(storage.total)} + + + ${msg("Storage in active orgs")}: + ${html`${this.localize.bytes(storage.active)}`} + ${msg("Storage in inactive orgs")}: + ${html`${this.localize.bytes( + storage.total - storage.active, + )}`} + + + + ${msg("Data Stored")} + + +
  • +
`; + }); + } +} diff --git a/frontend/src/features/index.ts b/frontend/src/features/index.ts index 3b0a50aa..635b4a96 100644 --- a/frontend/src/features/index.ts +++ b/frontend/src/features/index.ts @@ -5,3 +5,5 @@ import "./collections"; import "./crawl-workflows"; import "./org"; import "./qa"; + +import("./admin"); diff --git a/frontend/src/pages/admin.ts b/frontend/src/pages/admin.ts index cfa343e3..d7599d51 100644 --- a/frontend/src/pages/admin.ts +++ b/frontend/src/pages/admin.ts @@ -156,6 +156,9 @@ export class Admin extends BtrixElement {
+

${msg("Invite User to Org")}