| 1 |
import React from "react"; |
| 2 |
import { __, sprintf } from "@wordpress/i18n"; |
| 3 |
import { useQuery } from "@tanstack/react-query"; |
| 4 |
|
| 5 |
import { getApps, getConnection, getHealth } from "../utils/api"; |
| 6 |
import { formatDate, relativeTime, toMillis } from "../utils/format"; |
| 7 |
import { APPS_KEY } from "./ConnectedApps"; |
| 8 |
import { CONNECTION_KEY } from "./ConnectionConfig"; |
| 9 |
import { HEALTH_KEY } from "./ConnectionHealth"; |
| 10 |
import { IconClock, IconCode, IconGrid, IconPlug } from "./Icons"; |
| 11 |
|
| 12 |
/** |
| 13 |
* The four KPI tiles (ADR-062). |
| 14 |
* |
| 15 |
* Every value is read from a payload the page already fetches — the same three |
| 16 |
* React Query keys the sections below use, so this row costs no extra request: |
| 17 |
* an identical key and query function is served from the shared cache. |
| 18 |
* |
| 19 |
* **No sparklines and no delta chips.** The approved mock-up draws both, and |
| 20 |
* they are decoration over data that does not exist: the server keeps one |
| 21 |
* `last_used` stamp per credential and no history whatsoever, so a trend line |
| 22 |
* here could only be invented. |
| 23 |
* |
| 24 |
* @since 4.9.0 |
| 25 |
*/ |
| 26 |
|
| 27 |
/** |
| 28 |
* One tile. The accent arrives as custom properties so the stylesheet decides |
| 29 |
* what happens to it in dark mode, the way the client cards do (ADR-054). |
| 30 |
*/ |
| 31 |
const Tile = ({ label, Icon, accent, accentDark, value, valueSm, note }) => ( |
| 32 |
<div className="bda-kpi bd-mcp-kpi"> |
| 33 |
<div className="bda-kpi-top"> |
| 34 |
<span className="bda-kpi-label">{label}</span> |
| 35 |
<span |
| 36 |
className="bda-kpi-ico bd-mcp-kpi__ico" |
| 37 |
style={{ |
| 38 |
"--bd-accent": accent, |
| 39 |
"--bd-accent-dark": accentDark || accent, |
| 40 |
"--bd-accent-tint": `${accent}1a`, |
| 41 |
}} |
| 42 |
> |
| 43 |
<Icon size={14} /> |
| 44 |
</span> |
| 45 |
</div> |
| 46 |
<div className="bda-kpi-row"> |
| 47 |
<span className={`bda-kpi-value${valueSm ? " bd-mcp-kpi__value--sm" : ""}`}>{value}</span> |
| 48 |
{note && <span className="bd-mcp-kpi__note">{note}</span>} |
| 49 |
</div> |
| 50 |
</div> |
| 51 |
); |
| 52 |
|
| 53 |
/** |
| 54 |
* The analytics skeleton for a tile that is still waiting on its query |
| 55 |
* (`KpiSkeleton` in Pro's `analytics/components/common/ui.js`), so the row |
| 56 |
* arrives at its final height in one step instead of growing under the reader. |
| 57 |
*/ |
| 58 |
const TileSkeleton = () => ( |
| 59 |
<div className="bda-kpi bd-mcp-kpi"> |
| 60 |
<div className="bda-skel" style={{ width: "50%", height: 10, marginBottom: 12 }} /> |
| 61 |
<div className="bda-kpi-row"> |
| 62 |
<div className="bda-skel" style={{ width: 120, height: 26 }} /> |
| 63 |
</div> |
| 64 |
</div> |
| 65 |
); |
| 66 |
|
| 67 |
export default function KpiRow({ status }) { |
| 68 |
const connection = useQuery({ queryKey: [CONNECTION_KEY], queryFn: getConnection }); |
| 69 |
const health = useQuery({ queryKey: [HEALTH_KEY], queryFn: getHealth }); |
| 70 |
const apps = useQuery({ queryKey: [APPS_KEY], queryFn: getApps }); |
| 71 |
|
| 72 |
const connectedAt = connection.data?.connected_at || null; |
| 73 |
const toolCount = health.data?.abilities?.betterdocs; |
| 74 |
const appRows = Array.isArray(apps.data?.oauth_apps) ? apps.data.oauth_apps : []; |
| 75 |
|
| 76 |
// The newest stamp anything holds: the connection token's own last use, and |
| 77 |
// each OAuth app's. There is no other record of activity on this server. |
| 78 |
// Compared as milliseconds, but carried forward in the shape it arrived in — |
| 79 |
// `relativeTime` does its own conversion, and handing it a value already in |
| 80 |
// milliseconds would multiply it by a thousand a second time. |
| 81 |
const lastActivity = [connection.data?.last_used, ...appRows.map((app) => app.last_used)] |
| 82 |
.map((value) => ({ value, ms: toMillis(value) })) |
| 83 |
.filter((stamp) => stamp.ms !== null) |
| 84 |
.reduce((newest, stamp) => (newest === null || stamp.ms > newest.ms ? stamp : newest), null); |
| 85 |
|
| 86 |
return ( |
| 87 |
<div className="bda-kpi-grid bd-mcp-kpis"> |
| 88 |
{connection.isLoading ? ( |
| 89 |
<TileSkeleton /> |
| 90 |
) : ( |
| 91 |
<Tile |
| 92 |
label={__("Server status", "betterdocs")} |
| 93 |
Icon={IconPlug} |
| 94 |
accent="#00b884" |
| 95 |
value={status.label} |
| 96 |
valueSm |
| 97 |
note={ |
| 98 |
connectedAt |
| 99 |
? sprintf( |
| 100 |
/* translators: %s: the date this site's connection was established. */ |
| 101 |
__("since %s", "betterdocs"), |
| 102 |
formatDate(connectedAt), |
| 103 |
) |
| 104 |
: null |
| 105 |
} |
| 106 |
/> |
| 107 |
)} |
| 108 |
|
| 109 |
{health.isLoading || typeof toolCount !== "number" ? ( |
| 110 |
<TileSkeleton /> |
| 111 |
) : ( |
| 112 |
<Tile |
| 113 |
label={__("Registered tools", "betterdocs")} |
| 114 |
Icon={IconCode} |
| 115 |
accent="#3b82f6" |
| 116 |
accentDark="#60a5fa" |
| 117 |
value={String(toolCount)} |
| 118 |
/> |
| 119 |
)} |
| 120 |
|
| 121 |
{apps.isLoading ? ( |
| 122 |
<TileSkeleton /> |
| 123 |
) : ( |
| 124 |
<Tile |
| 125 |
label={__("Connected apps", "betterdocs")} |
| 126 |
Icon={IconGrid} |
| 127 |
accent="#8b5cf6" |
| 128 |
accentDark="#a78bfa" |
| 129 |
value={String(appRows.length)} |
| 130 |
/> |
| 131 |
)} |
| 132 |
|
| 133 |
{connection.isLoading || apps.isLoading ? ( |
| 134 |
<TileSkeleton /> |
| 135 |
) : ( |
| 136 |
<Tile |
| 137 |
label={__("Last activity", "betterdocs")} |
| 138 |
Icon={IconClock} |
| 139 |
accent="#f59e0b" |
| 140 |
accentDark="#fbbf24" |
| 141 |
value={relativeTime(lastActivity ? lastActivity.value : null)} |
| 142 |
valueSm |
| 143 |
/> |
| 144 |
)} |
| 145 |
</div> |
| 146 |
); |
| 147 |
} |
| 148 |
|