fix(dashboard): make quota windows first-class (#28)
This commit is contained in:
@@ -140,12 +140,6 @@ function usagePeak(row: UsageRow): number {
|
|||||||
return Math.max(row.h5pct, row.d7pct);
|
return Math.max(row.h5pct, row.d7pct);
|
||||||
}
|
}
|
||||||
|
|
||||||
function usageRemaining(row: UsageRow): number | null {
|
|
||||||
const peak = usagePeak(row);
|
|
||||||
if (peak < 0) return null;
|
|
||||||
return Math.max(0, 100 - peak);
|
|
||||||
}
|
|
||||||
|
|
||||||
function usageLimitWindow(row: UsageRow): UsageLimitWindow {
|
function usageLimitWindow(row: UsageRow): UsageLimitWindow {
|
||||||
return row.d7pct >= row.h5pct ? 'd7' : 'h5';
|
return row.d7pct >= row.h5pct ? 'd7' : 'h5';
|
||||||
}
|
}
|
||||||
@@ -187,8 +181,8 @@ function usageNameParts(row: UsageRow): {
|
|||||||
return { account: cleaned, plan: null };
|
return { account: cleaned, plan: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
function usageLimitReset(row: UsageRow): string {
|
function usageWindowReset(row: UsageRow, window: UsageLimitWindow): string {
|
||||||
return (usageLimitWindow(row) === 'd7' ? row.d7reset : row.h5reset).trim();
|
return (window === 'd7' ? row.d7reset : row.h5reset).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function usageBurnRate(row: UsageRow): number | null {
|
function usageBurnRate(row: UsageRow): number | null {
|
||||||
@@ -929,50 +923,36 @@ function RoomPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UsageRemainingMeter({
|
function UsageQuotaMeter({
|
||||||
row,
|
row,
|
||||||
rowName,
|
rowName,
|
||||||
|
window,
|
||||||
t,
|
t,
|
||||||
}: {
|
}: {
|
||||||
row: UsageRow;
|
row: UsageRow;
|
||||||
rowName: string;
|
rowName: string;
|
||||||
|
window: UsageLimitWindow;
|
||||||
t: Messages;
|
t: Messages;
|
||||||
}) {
|
}) {
|
||||||
const remaining = usageRemaining(row);
|
const remaining = usageWindowRemaining(row, window);
|
||||||
const window = usageLimitWindow(row);
|
const reset = usageWindowReset(row, window);
|
||||||
const h5Remaining = usageWindowRemaining(row, 'h5');
|
const tightest = usageLimitWindow(row) === window;
|
||||||
const d7Remaining = usageWindowRemaining(row, 'd7');
|
const label = t.usage.quota[window];
|
||||||
const reset = usageLimitReset(row);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="usage-remaining">
|
<div className={`usage-quota ${tightest ? 'usage-quota-tight' : ''}`}>
|
||||||
<div>
|
<div>
|
||||||
<span>{t.usage.remaining}</span>
|
<span>{label}</span>
|
||||||
<strong>{remaining === null ? '-' : formatPct(remaining)}</strong>
|
<strong>{remaining === null ? '-' : formatPct(remaining)}</strong>
|
||||||
</div>
|
</div>
|
||||||
<progress
|
<progress
|
||||||
aria-label={`${rowName} ${t.usage.remaining} ${
|
aria-label={`${rowName} ${label} ${
|
||||||
remaining === null ? '-' : formatPct(remaining)
|
remaining === null ? '-' : formatPct(remaining)
|
||||||
}`}
|
}`}
|
||||||
max={100}
|
max={100}
|
||||||
value={remaining ?? 0}
|
value={remaining ?? 0}
|
||||||
/>
|
/>
|
||||||
<div className="quota-pair" aria-label={`${rowName} quota windows`}>
|
<small>{reset ? `${t.usage.reset} ${reset}` : t.usage.noReset}</small>
|
||||||
<span className={window === 'h5' ? 'is-tight' : undefined}>
|
|
||||||
<b>{t.usage.quota.h5}</b>
|
|
||||||
<em>{h5Remaining === null ? '-' : formatPct(h5Remaining)}</em>
|
|
||||||
<progress max={100} value={h5Remaining ?? 0} />
|
|
||||||
</span>
|
|
||||||
<span className={window === 'd7' ? 'is-tight' : undefined}>
|
|
||||||
<b>{t.usage.quota.d7}</b>
|
|
||||||
<em>{d7Remaining === null ? '-' : formatPct(d7Remaining)}</em>
|
|
||||||
<progress max={100} value={d7Remaining ?? 0} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<small>
|
|
||||||
{remaining === null ? '-' : t.usage.limitBasis[window]}
|
|
||||||
{reset ? ` · ${t.usage.reset} ${reset}` : ` · ${t.usage.noReset}`}
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1017,8 +997,13 @@ function UsagePanel({
|
|||||||
const focusValue = focusRows
|
const focusValue = focusRows
|
||||||
.map((row) => {
|
.map((row) => {
|
||||||
const { account } = usageNameParts(row);
|
const { account } = usageNameParts(row);
|
||||||
const remaining = usageRemaining(row);
|
const h5Remaining = usageWindowRemaining(row, 'h5');
|
||||||
return `${account} ${remaining === null ? '-' : formatPct(remaining)}`;
|
const d7Remaining = usageWindowRemaining(row, 'd7');
|
||||||
|
return `${account} ${t.usage.quota.h5} ${
|
||||||
|
h5Remaining === null ? '-' : formatPct(h5Remaining)
|
||||||
|
} · ${t.usage.quota.d7} ${
|
||||||
|
d7Remaining === null ? '-' : formatPct(d7Remaining)
|
||||||
|
}`;
|
||||||
})
|
})
|
||||||
.join(' · ');
|
.join(' · ');
|
||||||
const groups = [
|
const groups = [
|
||||||
@@ -1050,7 +1035,8 @@ function UsagePanel({
|
|||||||
<div className="usage-matrix" role="table" aria-label={t.panels.usage}>
|
<div className="usage-matrix" role="table" aria-label={t.panels.usage}>
|
||||||
<div className="usage-matrix-head" role="row">
|
<div className="usage-matrix-head" role="row">
|
||||||
<span>{t.usage.usage}</span>
|
<span>{t.usage.usage}</span>
|
||||||
<span>{t.usage.remaining}</span>
|
<span>{t.usage.quota.h5}</span>
|
||||||
|
<span>{t.usage.quota.d7}</span>
|
||||||
<span>{t.usage.speed}</span>
|
<span>{t.usage.speed}</span>
|
||||||
</div>
|
</div>
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
@@ -1077,7 +1063,18 @@ function UsagePanel({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UsageRemainingMeter row={row} rowName={account} t={t} />
|
<UsageQuotaMeter
|
||||||
|
row={row}
|
||||||
|
rowName={account}
|
||||||
|
window="h5"
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
<UsageQuotaMeter
|
||||||
|
row={row}
|
||||||
|
rowName={account}
|
||||||
|
window="d7"
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
<UsageSpeed row={row} t={t} />
|
<UsageSpeed row={row} t={t} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -622,7 +622,7 @@ td small {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.usage-summary span,
|
.usage-summary span,
|
||||||
.usage-remaining small {
|
.usage-quota small {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,10 +642,9 @@ td small {
|
|||||||
.usage-matrix-head,
|
.usage-matrix-head,
|
||||||
.usage-row {
|
.usage-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(132px, 0.78fr) minmax(0, 1fr) minmax(
|
grid-template-columns:
|
||||||
96px,
|
minmax(132px, 0.82fr) minmax(82px, 0.42fr) minmax(82px, 0.42fr)
|
||||||
0.46fr
|
minmax(84px, 0.34fr);
|
||||||
);
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -693,7 +692,7 @@ td small {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.usage-account,
|
.usage-account,
|
||||||
.usage-remaining div {
|
.usage-quota div {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -723,7 +722,7 @@ td small {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-remaining span,
|
.usage-quota span,
|
||||||
.usage-speed span {
|
.usage-speed span {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -732,57 +731,42 @@ td small {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-remaining,
|
.usage-quota,
|
||||||
.usage-speed {
|
.usage-speed {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quota-pair {
|
.usage-quota {
|
||||||
display: grid;
|
padding: 6px;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
border-radius: 13px;
|
||||||
gap: 5px;
|
background: rgba(43, 55, 38, 0.035);
|
||||||
}
|
}
|
||||||
|
|
||||||
.quota-pair span {
|
.usage-quota-tight {
|
||||||
display: grid;
|
background: rgba(191, 95, 44, 0.1);
|
||||||
min-width: 0;
|
|
||||||
gap: 3px;
|
|
||||||
padding: 5px;
|
|
||||||
border-radius: 11px;
|
|
||||||
background: rgba(43, 55, 38, 0.045);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quota-pair span.is-tight {
|
|
||||||
background: rgba(191, 95, 44, 0.11);
|
|
||||||
box-shadow: inset 0 0 0 1px rgba(191, 95, 44, 0.16);
|
box-shadow: inset 0 0 0 1px rgba(191, 95, 44, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
.quota-pair b,
|
.usage-quota strong {
|
||||||
.quota-pair em {
|
font-size: 17px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-quota small {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
color: var(--muted);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-style: normal;
|
|
||||||
font-weight: 900;
|
|
||||||
line-height: 1;
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quota-pair b {
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quota-pair progress {
|
|
||||||
height: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.usage-speed {
|
.usage-speed {
|
||||||
min-height: 54px;
|
min-height: 48px;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
padding: 8px;
|
padding: 6px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: rgba(43, 55, 38, 0.045);
|
background: rgba(43, 55, 38, 0.045);
|
||||||
}
|
}
|
||||||
@@ -1603,8 +1587,10 @@ progress::-moz-progress-bar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.usage-row {
|
.usage-row {
|
||||||
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1fr) minmax(90px, 0.5fr);
|
grid-template-columns:
|
||||||
gap: 6px;
|
minmax(86px, 0.86fr) minmax(62px, 0.54fr) minmax(62px, 0.54fr)
|
||||||
|
minmax(58px, 0.4fr);
|
||||||
|
gap: 5px;
|
||||||
padding: 9px;
|
padding: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1613,30 +1599,26 @@ progress::-moz-progress-bar {
|
|||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-remaining,
|
.usage-quota,
|
||||||
.usage-speed {
|
.usage-speed {
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-remaining div {
|
.usage-quota {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-quota div {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-remaining small {
|
.usage-quota small {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quota-pair {
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quota-pair span {
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-card-grid {
|
.record-card-grid {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
@@ -1732,6 +1714,14 @@ progress::-moz-progress-bar {
|
|||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usage-speed {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
min-height: auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.skeleton-grid,
|
.skeleton-grid,
|
||||||
.ops-strip {
|
.ops-strip {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user