@acarmisc/backstage-plugin-litellm 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/budget.d.ts +22 -0
- package/dist/components/LiteLLMBudgetGauges.d.ts +27 -0
- package/dist/components/ui.d.ts +20 -0
- package/dist/index.cjs.js +311 -1
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +311 -1
- package/dist/index.esm.js.map +4 -4
- package/package.json +1 -1
package/dist/budget.d.ts
CHANGED
|
@@ -45,6 +45,28 @@ export interface BudgetSummary {
|
|
|
45
45
|
* team falls back to the owner's personal budget.
|
|
46
46
|
*/
|
|
47
47
|
export declare function buildBudgetSummary(user: UserInfo | null, teams: TeamInfo[], keys: VirtualKey[], maxKeys?: number): BudgetSummary;
|
|
48
|
+
/** One gauge in the condensed homepage card — a single enforcement level. */
|
|
49
|
+
export interface BudgetGauge {
|
|
50
|
+
kind: 'key' | 'user' | 'team';
|
|
51
|
+
/**
|
|
52
|
+
* The limit of this level nearest its cap, or undefined when the user has
|
|
53
|
+
* no cap at this level (render an empty gauge).
|
|
54
|
+
*/
|
|
55
|
+
limit?: BudgetLimit;
|
|
56
|
+
/** How many limits apply at this level (1 for a personal budget). */
|
|
57
|
+
count: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Collapses a `BudgetSummary` to exactly one gauge per enforcement level —
|
|
61
|
+
* key, user, team — carrying the limit nearest its cap and how many limits
|
|
62
|
+
* that level holds. Every level is always returned, with `limit` undefined
|
|
63
|
+
* when the user has no cap there, so the caller can render a stable row.
|
|
64
|
+
*
|
|
65
|
+
* Pass a summary built with a large `maxKeys` (e.g. `keys.length`) so the
|
|
66
|
+
* nearest key is present even when it falls outside the display cap; a key
|
|
67
|
+
* count hidden by that cap is still reflected in `count`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildBudgetGauges(summary: BudgetSummary): BudgetGauge[];
|
|
48
70
|
/**
|
|
49
71
|
* One-line summary of a `BudgetSummary`, for a collapsed/compact header:
|
|
50
72
|
* how many concrete limits the user is subject to, and the one closest to
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Condensed budget card for a homepage column: one ring gauge per
|
|
3
|
+
* enforcement level (key → personal → team), each showing the limit nearest
|
|
4
|
+
* its cap. Where the full `LiteLLMBudgetWidget` lists every limit as a
|
|
5
|
+
* spend-vs-cap meter, this trades detail for density — a single row of rings
|
|
6
|
+
* with a label and spend figure under each.
|
|
7
|
+
*
|
|
8
|
+
* Gauges are always rendered in the same order, with an empty ring when the
|
|
9
|
+
* user has no cap at that level, so the card keeps a stable shape as data
|
|
10
|
+
* loads and across users. When a level holds several limits, the ring shows
|
|
11
|
+
* the one closest to its cap and a "+N more" link counts the rest.
|
|
12
|
+
*/
|
|
13
|
+
import React from 'react';
|
|
14
|
+
export interface LiteLLMBudgetGaugesProps {
|
|
15
|
+
/** Optional title override. Defaults to 'Budget'. */
|
|
16
|
+
title?: string;
|
|
17
|
+
/** Ring diameter in px. Defaults to 72. */
|
|
18
|
+
size?: number;
|
|
19
|
+
/** Where the "+N more" key link points. Defaults to the Keys tab. */
|
|
20
|
+
keysHref?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Node rendered at the card bottom, below a divider — e.g. a "create key"
|
|
23
|
+
* button.
|
|
24
|
+
*/
|
|
25
|
+
action?: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
export declare const LiteLLMBudgetGauges: React.FC<LiteLLMBudgetGaugesProps>;
|
package/dist/components/ui.d.ts
CHANGED
|
@@ -156,6 +156,26 @@ export declare const Meter: React.FC<{
|
|
|
156
156
|
tone?: Tone;
|
|
157
157
|
height?: number;
|
|
158
158
|
}>;
|
|
159
|
+
/**
|
|
160
|
+
* Ring gauge for at-a-glance ratios. Hand-drawn SVG rather than MUI's
|
|
161
|
+
* `CircularProgress`: the track has to stay a neutral hairline (MUI tints it
|
|
162
|
+
* from the bar colour, so an empty ring reads as full), and the value may
|
|
163
|
+
* legitimately exceed 100% — the arc clamps at a full turn while the centre
|
|
164
|
+
* label keeps the real number.
|
|
165
|
+
*/
|
|
166
|
+
export declare const Gauge: React.FC<{
|
|
167
|
+
/** Share consumed, 0..100+ (unclamped for the label). */
|
|
168
|
+
value: number;
|
|
169
|
+
tone?: Tone;
|
|
170
|
+
/** Outer diameter in px. */
|
|
171
|
+
size?: number;
|
|
172
|
+
/** Ring thickness in px. */
|
|
173
|
+
thickness?: number;
|
|
174
|
+
/** Centre content; defaults to the rounded percentage. */
|
|
175
|
+
label?: React.ReactNode;
|
|
176
|
+
/** Small caption under the value, inside the ring. */
|
|
177
|
+
caption?: React.ReactNode;
|
|
178
|
+
}>;
|
|
159
179
|
/**
|
|
160
180
|
* Secondary navigation inside a card. Hand-rolled rather than a second set of
|
|
161
181
|
* `Tabs` so it reads as subordinate to the page tabs instead of competing
|
package/dist/index.cjs.js
CHANGED
|
@@ -335,7 +335,7 @@ function SegmentedControl({
|
|
|
335
335
|
})
|
|
336
336
|
);
|
|
337
337
|
}
|
|
338
|
-
var import_react, import_Box, import_ButtonBase, import_Paper, import_Typography, import_styles, MONTHS, fmtDateShort, fmtCompact, fmtUsdCompact, CHART_COLORS, SERIES, OTHER_COLOR, ChartTooltip, SeriesLegend, StatusPill, TagChip, SectionCard, ChartCard, MetricStrip, Stat, Meter, dataTableSx, quietIconButtonSx, EmptyState;
|
|
338
|
+
var import_react, import_Box, import_ButtonBase, import_Paper, import_Typography, import_styles, MONTHS, fmtDateShort, fmtCompact, fmtUsdCompact, CHART_COLORS, SERIES, OTHER_COLOR, ChartTooltip, SeriesLegend, StatusPill, TagChip, SectionCard, ChartCard, MetricStrip, Stat, Meter, Gauge, dataTableSx, quietIconButtonSx, EmptyState;
|
|
339
339
|
var init_ui = __esm({
|
|
340
340
|
"src/components/ui.tsx"() {
|
|
341
341
|
"use strict";
|
|
@@ -705,6 +705,102 @@ var init_ui = __esm({
|
|
|
705
705
|
}
|
|
706
706
|
)
|
|
707
707
|
);
|
|
708
|
+
Gauge = ({ value, tone = "accent", size = 72, thickness = 6, label, caption }) => {
|
|
709
|
+
const clamped = Math.max(0, Math.min(100, value ?? 0));
|
|
710
|
+
const r = (size - thickness) / 2;
|
|
711
|
+
const c = 2 * Math.PI * r;
|
|
712
|
+
const dash = clamped / 100 * c;
|
|
713
|
+
return /* @__PURE__ */ import_react.default.createElement(
|
|
714
|
+
import_Box.default,
|
|
715
|
+
{
|
|
716
|
+
sx: (theme) => ({
|
|
717
|
+
position: "relative",
|
|
718
|
+
width: size,
|
|
719
|
+
height: size,
|
|
720
|
+
flexShrink: 0,
|
|
721
|
+
color: toneColor(theme, tone)
|
|
722
|
+
})
|
|
723
|
+
},
|
|
724
|
+
/* @__PURE__ */ import_react.default.createElement(
|
|
725
|
+
import_Box.default,
|
|
726
|
+
{
|
|
727
|
+
component: "svg",
|
|
728
|
+
viewBox: `0 0 ${size} ${size}`,
|
|
729
|
+
width: size,
|
|
730
|
+
height: size,
|
|
731
|
+
sx: { display: "block", transform: "rotate(-90deg)" }
|
|
732
|
+
},
|
|
733
|
+
/* @__PURE__ */ import_react.default.createElement(
|
|
734
|
+
import_Box.default,
|
|
735
|
+
{
|
|
736
|
+
component: "circle",
|
|
737
|
+
cx: size / 2,
|
|
738
|
+
cy: size / 2,
|
|
739
|
+
r,
|
|
740
|
+
fill: "none",
|
|
741
|
+
strokeWidth: thickness,
|
|
742
|
+
sx: (theme) => ({
|
|
743
|
+
stroke: (0, import_styles.alpha)(
|
|
744
|
+
theme.palette.text.primary,
|
|
745
|
+
theme.palette.mode === "dark" ? 0.14 : 0.09
|
|
746
|
+
)
|
|
747
|
+
})
|
|
748
|
+
}
|
|
749
|
+
),
|
|
750
|
+
/* @__PURE__ */ import_react.default.createElement(
|
|
751
|
+
import_Box.default,
|
|
752
|
+
{
|
|
753
|
+
component: "circle",
|
|
754
|
+
cx: size / 2,
|
|
755
|
+
cy: size / 2,
|
|
756
|
+
r,
|
|
757
|
+
fill: "none",
|
|
758
|
+
strokeWidth: thickness,
|
|
759
|
+
strokeLinecap: "round",
|
|
760
|
+
strokeDasharray: `${dash} ${c - dash}`,
|
|
761
|
+
sx: (theme) => ({
|
|
762
|
+
stroke: "currentColor",
|
|
763
|
+
transition: theme.transitions.create("stroke-dasharray")
|
|
764
|
+
})
|
|
765
|
+
}
|
|
766
|
+
)
|
|
767
|
+
),
|
|
768
|
+
/* @__PURE__ */ import_react.default.createElement(
|
|
769
|
+
import_Box.default,
|
|
770
|
+
{
|
|
771
|
+
sx: {
|
|
772
|
+
position: "absolute",
|
|
773
|
+
inset: 0,
|
|
774
|
+
display: "flex",
|
|
775
|
+
flexDirection: "column",
|
|
776
|
+
alignItems: "center",
|
|
777
|
+
justifyContent: "center",
|
|
778
|
+
lineHeight: 1
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
/* @__PURE__ */ import_react.default.createElement(
|
|
782
|
+
import_Typography.default,
|
|
783
|
+
{
|
|
784
|
+
sx: {
|
|
785
|
+
fontSize: size <= 64 ? 15 : 18,
|
|
786
|
+
fontWeight: 700,
|
|
787
|
+
fontVariantNumeric: "tabular-nums",
|
|
788
|
+
color: "text.primary"
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
label ?? `${Math.round(value)}%`
|
|
792
|
+
),
|
|
793
|
+
caption && /* @__PURE__ */ import_react.default.createElement(
|
|
794
|
+
import_Typography.default,
|
|
795
|
+
{
|
|
796
|
+
variant: "caption",
|
|
797
|
+
sx: { fontSize: 9.5, color: "text.secondary", mt: 0.25 }
|
|
798
|
+
},
|
|
799
|
+
caption
|
|
800
|
+
)
|
|
801
|
+
)
|
|
802
|
+
);
|
|
803
|
+
};
|
|
708
804
|
dataTableSx = (theme) => ({
|
|
709
805
|
// Element selectors, not `.MuiTableCell-head`: host apps may set a MUI
|
|
710
806
|
// classname prefix (Backstage ships `v5-`), which breaks global class targeting.
|
|
@@ -2835,6 +2931,22 @@ function buildBudgetSummary(user, teams, keys, maxKeys = 3) {
|
|
|
2835
2931
|
teams: teamLimits
|
|
2836
2932
|
};
|
|
2837
2933
|
}
|
|
2934
|
+
function buildBudgetGauges(summary) {
|
|
2935
|
+
const byLevel = {
|
|
2936
|
+
key: summary.keys,
|
|
2937
|
+
user: summary.user ? [summary.user] : [],
|
|
2938
|
+
team: summary.teams
|
|
2939
|
+
};
|
|
2940
|
+
return GAUGE_KINDS.map((kind) => {
|
|
2941
|
+
const limits = byLevel[kind];
|
|
2942
|
+
const count = kind === "key" ? limits.length + summary.hiddenBudgetedKeys : limits.length;
|
|
2943
|
+
return {
|
|
2944
|
+
kind,
|
|
2945
|
+
count,
|
|
2946
|
+
limit: limits.length ? limits.reduce((a, b) => b.pct > a.pct ? b : a) : void 0
|
|
2947
|
+
};
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2838
2950
|
function budgetHeadline(summary) {
|
|
2839
2951
|
const all = [
|
|
2840
2952
|
...summary.keys,
|
|
@@ -2861,9 +2973,11 @@ function budgetTone2(pct) {
|
|
|
2861
2973
|
if (pct >= 80) return "warning";
|
|
2862
2974
|
return "accent";
|
|
2863
2975
|
}
|
|
2976
|
+
var GAUGE_KINDS;
|
|
2864
2977
|
var init_budget = __esm({
|
|
2865
2978
|
"src/budget.ts"() {
|
|
2866
2979
|
"use strict";
|
|
2980
|
+
GAUGE_KINDS = ["key", "user", "team"];
|
|
2867
2981
|
}
|
|
2868
2982
|
});
|
|
2869
2983
|
|
|
@@ -4379,6 +4493,7 @@ __export(index_exports, {
|
|
|
4379
4493
|
DashboardHeader: () => DashboardHeader,
|
|
4380
4494
|
GenerateKeyDialog: () => GenerateKeyDialog,
|
|
4381
4495
|
KeysTable: () => KeysTable,
|
|
4496
|
+
LiteLLMBudgetGauges: () => LiteLLMBudgetGauges,
|
|
4382
4497
|
LiteLLMBudgetWidget: () => LiteLLMBudgetWidget,
|
|
4383
4498
|
LiteLLMHomeWidget: () => LiteLLMHomeWidget,
|
|
4384
4499
|
LiteLLMPage: () => LiteLLMPage,
|
|
@@ -4533,6 +4648,201 @@ var LiteLLMHomeWidget = ({
|
|
|
4533
4648
|
|
|
4534
4649
|
// src/index.ts
|
|
4535
4650
|
init_LiteLLMBudgetWidget();
|
|
4651
|
+
|
|
4652
|
+
// src/components/LiteLLMBudgetGauges.tsx
|
|
4653
|
+
var import_react14 = __toESM(require("react"));
|
|
4654
|
+
var import_Paper9 = __toESM(require("@mui/material/Paper"));
|
|
4655
|
+
var import_Box13 = __toESM(require("@mui/material/Box"));
|
|
4656
|
+
var import_Typography13 = __toESM(require("@mui/material/Typography"));
|
|
4657
|
+
var import_CircularProgress8 = __toESM(require("@mui/material/CircularProgress"));
|
|
4658
|
+
var import_Alert8 = __toESM(require("@mui/material/Alert"));
|
|
4659
|
+
var import_core_plugin_api6 = require("@backstage/core-plugin-api");
|
|
4660
|
+
var import_core_components2 = require("@backstage/core-components");
|
|
4661
|
+
init_api();
|
|
4662
|
+
init_format();
|
|
4663
|
+
init_ui();
|
|
4664
|
+
init_budget();
|
|
4665
|
+
var DEFAULT_KEYS_HREF = "/litellm?tab=keys";
|
|
4666
|
+
var LEVEL = {
|
|
4667
|
+
key: { name: "Key", none: "No key budget" },
|
|
4668
|
+
user: { name: "User", none: "No personal budget" },
|
|
4669
|
+
team: { name: "Team", none: "No team budget" }
|
|
4670
|
+
};
|
|
4671
|
+
function fmtUsdShort(n) {
|
|
4672
|
+
const v = n ?? 0;
|
|
4673
|
+
if (v >= 100 && Number.isInteger(v)) return `$${v}`;
|
|
4674
|
+
return fmtUsd(v);
|
|
4675
|
+
}
|
|
4676
|
+
function spendCaption(limit) {
|
|
4677
|
+
if (limit.hidden) return "hidden by admin";
|
|
4678
|
+
return `${fmtUsdShort(limit.spend)} / ${fmtUsdShort(limit.budget)}`;
|
|
4679
|
+
}
|
|
4680
|
+
function resetLabel(limit) {
|
|
4681
|
+
const window2 = fmtBudgetDuration(limit.budgetDuration);
|
|
4682
|
+
return window2 ? `resets ${window2}` : "never resets";
|
|
4683
|
+
}
|
|
4684
|
+
var LevelGauge = ({
|
|
4685
|
+
gauge,
|
|
4686
|
+
size,
|
|
4687
|
+
keysHref
|
|
4688
|
+
}) => {
|
|
4689
|
+
const { name, none } = LEVEL[gauge.kind];
|
|
4690
|
+
const limit = gauge.limit;
|
|
4691
|
+
const tone = limit ? budgetTone2(limit.pct) : "neutral";
|
|
4692
|
+
const extra = gauge.kind === "key" ? Math.max(0, gauge.count - 1) : 0;
|
|
4693
|
+
return /* @__PURE__ */ import_react14.default.createElement(
|
|
4694
|
+
import_Box13.default,
|
|
4695
|
+
{
|
|
4696
|
+
sx: {
|
|
4697
|
+
display: "flex",
|
|
4698
|
+
flexDirection: "column",
|
|
4699
|
+
alignItems: "center",
|
|
4700
|
+
minWidth: 0,
|
|
4701
|
+
flex: 1
|
|
4702
|
+
}
|
|
4703
|
+
},
|
|
4704
|
+
/* @__PURE__ */ import_react14.default.createElement(
|
|
4705
|
+
Gauge,
|
|
4706
|
+
{
|
|
4707
|
+
value: limit?.pct ?? 0,
|
|
4708
|
+
tone,
|
|
4709
|
+
size,
|
|
4710
|
+
label: limit ? `${Math.round(limit.pct)}%` : "\u2014"
|
|
4711
|
+
}
|
|
4712
|
+
),
|
|
4713
|
+
/* @__PURE__ */ import_react14.default.createElement(
|
|
4714
|
+
import_Typography13.default,
|
|
4715
|
+
{
|
|
4716
|
+
sx: (theme) => ({
|
|
4717
|
+
mt: 0.75,
|
|
4718
|
+
fontSize: 10.5,
|
|
4719
|
+
fontWeight: 700,
|
|
4720
|
+
letterSpacing: "0.08em",
|
|
4721
|
+
textTransform: "uppercase",
|
|
4722
|
+
color: tone === "neutral" ? theme.palette.text.secondary : void 0
|
|
4723
|
+
})
|
|
4724
|
+
},
|
|
4725
|
+
name
|
|
4726
|
+
),
|
|
4727
|
+
limit ? /* @__PURE__ */ import_react14.default.createElement(import_react14.default.Fragment, null, /* @__PURE__ */ import_react14.default.createElement(
|
|
4728
|
+
import_Typography13.default,
|
|
4729
|
+
{
|
|
4730
|
+
variant: "caption",
|
|
4731
|
+
title: limit.sublabel ? `${limit.label} \xB7 ${limit.sublabel}` : limit.label,
|
|
4732
|
+
sx: {
|
|
4733
|
+
mt: 0.25,
|
|
4734
|
+
fontSize: 11,
|
|
4735
|
+
fontWeight: 600,
|
|
4736
|
+
maxWidth: "100%",
|
|
4737
|
+
overflow: "hidden",
|
|
4738
|
+
textOverflow: "ellipsis",
|
|
4739
|
+
whiteSpace: "nowrap"
|
|
4740
|
+
}
|
|
4741
|
+
},
|
|
4742
|
+
limit.label
|
|
4743
|
+
), /* @__PURE__ */ import_react14.default.createElement(
|
|
4744
|
+
import_Typography13.default,
|
|
4745
|
+
{
|
|
4746
|
+
variant: "caption",
|
|
4747
|
+
color: "text.secondary",
|
|
4748
|
+
sx: {
|
|
4749
|
+
fontSize: 10.5,
|
|
4750
|
+
fontVariantNumeric: "tabular-nums",
|
|
4751
|
+
textAlign: "center"
|
|
4752
|
+
}
|
|
4753
|
+
},
|
|
4754
|
+
spendCaption(limit)
|
|
4755
|
+
), extra > 0 ? /* @__PURE__ */ import_react14.default.createElement(
|
|
4756
|
+
import_Typography13.default,
|
|
4757
|
+
{
|
|
4758
|
+
component: import_core_components2.Link,
|
|
4759
|
+
to: keysHref,
|
|
4760
|
+
variant: "caption",
|
|
4761
|
+
sx: {
|
|
4762
|
+
fontSize: 10.5,
|
|
4763
|
+
color: "primary.main",
|
|
4764
|
+
textDecoration: "none",
|
|
4765
|
+
"&:hover": { textDecoration: "underline" }
|
|
4766
|
+
}
|
|
4767
|
+
},
|
|
4768
|
+
"+",
|
|
4769
|
+
extra,
|
|
4770
|
+
" more"
|
|
4771
|
+
) : /* @__PURE__ */ import_react14.default.createElement(import_Typography13.default, { variant: "caption", color: "text.secondary", sx: { fontSize: 10.5 } }, resetLabel(limit))) : /* @__PURE__ */ import_react14.default.createElement(
|
|
4772
|
+
import_Typography13.default,
|
|
4773
|
+
{
|
|
4774
|
+
variant: "caption",
|
|
4775
|
+
color: "text.secondary",
|
|
4776
|
+
sx: { mt: 0.25, fontSize: 10.5, textAlign: "center" }
|
|
4777
|
+
},
|
|
4778
|
+
none
|
|
4779
|
+
)
|
|
4780
|
+
);
|
|
4781
|
+
};
|
|
4782
|
+
var LiteLLMBudgetGauges = ({
|
|
4783
|
+
title = "Budget",
|
|
4784
|
+
size = 72,
|
|
4785
|
+
keysHref = DEFAULT_KEYS_HREF,
|
|
4786
|
+
action
|
|
4787
|
+
}) => {
|
|
4788
|
+
const api = (0, import_core_plugin_api6.useApi)(liteLlmApiRef);
|
|
4789
|
+
const [loading, setLoading] = (0, import_react14.useState)(true);
|
|
4790
|
+
const [error, setError] = (0, import_react14.useState)(null);
|
|
4791
|
+
const [user, setUser] = (0, import_react14.useState)(null);
|
|
4792
|
+
const [teams, setTeams] = (0, import_react14.useState)([]);
|
|
4793
|
+
const [keys, setKeys] = (0, import_react14.useState)([]);
|
|
4794
|
+
(0, import_react14.useEffect)(() => {
|
|
4795
|
+
let cancelled = false;
|
|
4796
|
+
setLoading(true);
|
|
4797
|
+
setError(null);
|
|
4798
|
+
Promise.allSettled([api.getUserInfo(), api.getTeams(), api.listKeys()]).then(
|
|
4799
|
+
([userResult, teamsResult, keysResult]) => {
|
|
4800
|
+
if (cancelled) return;
|
|
4801
|
+
if (userResult.status === "fulfilled") {
|
|
4802
|
+
setUser(userResult.value);
|
|
4803
|
+
} else {
|
|
4804
|
+
setUser(null);
|
|
4805
|
+
setError(
|
|
4806
|
+
userResult.reason?.message ?? "Failed to load your LiteLLM profile"
|
|
4807
|
+
);
|
|
4808
|
+
}
|
|
4809
|
+
setTeams(teamsResult.status === "fulfilled" ? teamsResult.value : []);
|
|
4810
|
+
setKeys(keysResult.status === "fulfilled" ? keysResult.value : []);
|
|
4811
|
+
setLoading(false);
|
|
4812
|
+
}
|
|
4813
|
+
);
|
|
4814
|
+
return () => {
|
|
4815
|
+
cancelled = true;
|
|
4816
|
+
};
|
|
4817
|
+
}, [api]);
|
|
4818
|
+
const summary = (0, import_react14.useMemo)(
|
|
4819
|
+
() => buildBudgetSummary(user, teams, keys, keys.length),
|
|
4820
|
+
[user, teams, keys]
|
|
4821
|
+
);
|
|
4822
|
+
const gauges = (0, import_react14.useMemo)(() => buildBudgetGauges(summary), [summary]);
|
|
4823
|
+
const headline = (0, import_react14.useMemo)(() => budgetHeadline(summary), [summary]);
|
|
4824
|
+
const summaryText = headline.count === 0 ? "no limits apply" : `${headline.count} limit${headline.count === 1 ? "" : "s"}`;
|
|
4825
|
+
return /* @__PURE__ */ import_react14.default.createElement(import_Paper9.default, { sx: { p: 2 } }, /* @__PURE__ */ import_react14.default.createElement(import_Box13.default, { sx: { display: "flex", alignItems: "center", gap: 1, mb: 1.5, minWidth: 0 } }, /* @__PURE__ */ import_react14.default.createElement(import_Typography13.default, { variant: "h6", lineHeight: 1.2, sx: { minWidth: 0, flex: 1 } }, title), !loading && !error && /* @__PURE__ */ import_react14.default.createElement(
|
|
4826
|
+
StatusPill,
|
|
4827
|
+
{
|
|
4828
|
+
label: summaryText,
|
|
4829
|
+
tone: budgetTone2(headline.closest?.pct ?? 0),
|
|
4830
|
+
dot: headline.count > 0
|
|
4831
|
+
}
|
|
4832
|
+
)), loading && /* @__PURE__ */ import_react14.default.createElement(import_Box13.default, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 110 }, /* @__PURE__ */ import_react14.default.createElement(import_CircularProgress8.default, { size: 28 })), !loading && error && /* @__PURE__ */ import_react14.default.createElement(import_Alert8.default, { severity: "error", sx: { mt: 0.5 } }, error), !loading && !error && /* @__PURE__ */ import_react14.default.createElement(import_Box13.default, { sx: { display: "flex", alignItems: "flex-start", gap: 1 } }, gauges.map((gauge) => /* @__PURE__ */ import_react14.default.createElement(LevelGauge, { key: gauge.kind, gauge, size, keysHref }))), action && /* @__PURE__ */ import_react14.default.createElement(
|
|
4833
|
+
import_Box13.default,
|
|
4834
|
+
{
|
|
4835
|
+
sx: (theme) => ({
|
|
4836
|
+
mt: 2,
|
|
4837
|
+
pt: 2,
|
|
4838
|
+
borderTop: `1px solid ${theme.palette.divider}`
|
|
4839
|
+
})
|
|
4840
|
+
},
|
|
4841
|
+
action
|
|
4842
|
+
));
|
|
4843
|
+
};
|
|
4844
|
+
|
|
4845
|
+
// src/index.ts
|
|
4536
4846
|
init_GenerateKeyDialog();
|
|
4537
4847
|
init_ManageTeamDialog();
|
|
4538
4848
|
init_api();
|