Skip to content

Commit 34abe88

Browse files
asizikovCopilot
andcommitted
feat: add products view with bar chart and breakdown table
Add a new Products sidebar item and view showing per-product usage with dual PRU/AIC horizontal bar charts and an expandable per-model breakdown table. The bars and table are rendered in separate cards. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 12e99cf commit 34abe88

3 files changed

Lines changed: 471 additions & 1 deletion

File tree

src/App.css

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,239 @@ td.cost-centers-view__num {
977977
outline-offset: 2px;
978978
}
979979

980+
.products-view {
981+
display: flex;
982+
flex-direction: column;
983+
gap: 20px;
984+
}
985+
986+
.products-view__header {
987+
display: flex;
988+
align-items: flex-end;
989+
justify-content: space-between;
990+
gap: 16px;
991+
flex-wrap: wrap;
992+
}
993+
994+
.products-view__titleGroup {
995+
display: flex;
996+
align-items: baseline;
997+
gap: 12px;
998+
flex-wrap: wrap;
999+
}
1000+
1001+
.products-view__heading {
1002+
margin: 0;
1003+
font-size: 18px;
1004+
color: #1f2328;
1005+
}
1006+
1007+
.products-view__count {
1008+
font-size: 13px;
1009+
color: #636c76;
1010+
}
1011+
1012+
.products-view__rateNote {
1013+
font-size: 13px;
1014+
color: #636c76;
1015+
}
1016+
1017+
.products-view__card {
1018+
display: flex;
1019+
flex-direction: column;
1020+
gap: 20px;
1021+
padding: 20px;
1022+
}
1023+
1024+
.products-view__legend {
1025+
display: flex;
1026+
flex-wrap: wrap;
1027+
gap: 14px;
1028+
font-size: 11px;
1029+
color: #636c76;
1030+
}
1031+
1032+
.products-view__legendItem {
1033+
display: inline-flex;
1034+
align-items: center;
1035+
gap: 5px;
1036+
}
1037+
1038+
.products-view__legendSwatch {
1039+
width: 10px;
1040+
height: 10px;
1041+
border-radius: 2px;
1042+
flex-shrink: 0;
1043+
}
1044+
1045+
.products-view__legendSwatch--pru {
1046+
background: #cf222e;
1047+
opacity: 0.7;
1048+
}
1049+
1050+
.products-view__legendSwatch--aiu {
1051+
background: #54aeff;
1052+
}
1053+
1054+
.products-view__bars {
1055+
display: flex;
1056+
flex-direction: column;
1057+
gap: 14px;
1058+
}
1059+
1060+
.products-view__barRow {
1061+
display: grid;
1062+
grid-template-columns: minmax(140px, 180px) minmax(0, 1fr);
1063+
gap: 14px;
1064+
align-items: start;
1065+
}
1066+
1067+
.products-view__barLabel {
1068+
display: flex;
1069+
align-items: center;
1070+
gap: 6px;
1071+
min-width: 0;
1072+
}
1073+
1074+
.products-view__barDot,
1075+
.products-view__tableDot {
1076+
width: 8px;
1077+
height: 8px;
1078+
border-radius: 2px;
1079+
flex-shrink: 0;
1080+
}
1081+
1082+
.products-view__barName,
1083+
.products-view__name {
1084+
font-size: 13px;
1085+
font-weight: 600;
1086+
color: #1f2328;
1087+
}
1088+
1089+
.products-view__barValues {
1090+
display: flex;
1091+
align-items: center;
1092+
gap: 8px;
1093+
min-width: 0;
1094+
}
1095+
1096+
.products-view__barStack {
1097+
display: flex;
1098+
flex-direction: column;
1099+
gap: 8px;
1100+
min-width: 0;
1101+
}
1102+
1103+
.products-view__barTrack {
1104+
flex: 1;
1105+
height: 14px;
1106+
border-radius: 4px;
1107+
overflow: hidden;
1108+
background: #eaeef2;
1109+
}
1110+
1111+
.products-view__barFill {
1112+
height: 100%;
1113+
border-radius: 4px;
1114+
min-width: 0;
1115+
}
1116+
1117+
.products-view__barFill--pru {
1118+
background: rgba(207, 34, 46, 0.7);
1119+
}
1120+
1121+
.products-view__barFill--aiu {
1122+
background: #54aeff;
1123+
}
1124+
1125+
.products-view__barValue {
1126+
min-width: 72px;
1127+
font-size: 10px;
1128+
font-weight: 500;
1129+
font-variant-numeric: tabular-nums;
1130+
white-space: nowrap;
1131+
}
1132+
1133+
.products-view__barValue--pru {
1134+
color: #cf222e;
1135+
}
1136+
1137+
.products-view__barValue--aiu {
1138+
color: #0969da;
1139+
}
1140+
1141+
.products-view__table th.products-view__num {
1142+
text-align: right;
1143+
}
1144+
1145+
.products-view__num {
1146+
text-align: right;
1147+
font-variant-numeric: tabular-nums;
1148+
}
1149+
1150+
.products-view__nameCell {
1151+
display: flex;
1152+
align-items: center;
1153+
gap: 6px;
1154+
}
1155+
1156+
.products-view__toggle {
1157+
display: inline-flex;
1158+
align-items: center;
1159+
gap: 6px;
1160+
border: none;
1161+
background: transparent;
1162+
padding: 0;
1163+
font: inherit;
1164+
color: inherit;
1165+
cursor: pointer;
1166+
}
1167+
1168+
.products-view__toggle:hover .products-view__name {
1169+
color: #0969da;
1170+
}
1171+
1172+
.products-view__toggle:focus-visible {
1173+
outline: 2px solid #6366f1;
1174+
outline-offset: 2px;
1175+
border-radius: 4px;
1176+
}
1177+
1178+
.products-view__chevron {
1179+
width: 12px;
1180+
color: #636c76;
1181+
font-size: 9px;
1182+
line-height: 1;
1183+
transition: transform 0.15s ease;
1184+
}
1185+
1186+
.products-view__chevron--expanded {
1187+
transform: rotate(90deg);
1188+
}
1189+
1190+
.products-view__productRow:hover td {
1191+
background: #fcfdff;
1192+
}
1193+
1194+
.products-view__modelRow td {
1195+
color: #57606a;
1196+
background: #ffffff;
1197+
}
1198+
1199+
.products-view__modelCell {
1200+
padding-left: 44px !important;
1201+
font-size: 12px;
1202+
color: #57606a;
1203+
}
1204+
1205+
.products-view__empty {
1206+
background: #ffffff;
1207+
border: 1px solid #d1d9e0;
1208+
border-radius: 6px;
1209+
padding: 16px;
1210+
color: #636c76;
1211+
}
1212+
9801213
.users-view__title {
9811214
display: flex;
9821215
align-items: baseline;
@@ -1333,6 +1566,15 @@ td.cost-centers-view__num {
13331566
grid-template-columns: 1fr;
13341567
}
13351568

1569+
.products-view__barRow {
1570+
grid-template-columns: 1fr;
1571+
gap: 8px;
1572+
}
1573+
1574+
.products-view__card {
1575+
padding: 16px;
1576+
}
1577+
13361578
.next-steps__grid {
13371579
grid-template-columns: 1fr;
13381580
}
@@ -1486,6 +1728,12 @@ td.cost-centers-view__num {
14861728
outline-offset: -2px;
14871729
}
14881730

1731+
.sidebar__divider {
1732+
border: none;
1733+
border-top: 1px solid #d1d9e0;
1734+
margin: 6px 0;
1735+
}
1736+
14891737
.sidebar__icon {
14901738
width: 18px;
14911739
height: 18px;

src/App.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import { OrganizationsView } from './views/OrganizationsView'
99
import { ModelsView } from './views/ModelsView'
1010
import { ReportGuideView } from './views/ReportGuideView'
1111
import { FaqView } from './views/FaqView'
12+
import { ProductsView } from './views/ProductsView'
1213
import { QuickStatsAggregator, type QuickStatsResult } from './pipeline/aggregators/quickStatsAggregator'
1314
import { ReportContextAggregator, type ReportContextResult } from './pipeline/aggregators/reportContextAggregator'
1415
import { DailyUsageAggregator, type DailyUsageData } from './pipeline/aggregators/dailyUsageAggregator'
1516
import { ModelUsageAggregator, type ModelUsageResult } from './pipeline/aggregators/modelUsageAggregator'
17+
import { ProductUsageAggregator, type ProductUsageResult } from './pipeline/aggregators/productUsageAggregator'
1618
import { CostCenterAggregator, type CostCenterResult } from './pipeline/aggregators/costCenterAggregator'
1719
import { OrganizationAggregator, type OrganizationResult } from './pipeline/aggregators/organizationAggregator'
1820
import { UserUsageAggregator, type UserUsageResult } from './pipeline/aggregators/userUsageAggregator'
@@ -55,9 +57,10 @@ function App() {
5557
const [fileName, setFileName] = useState<string | null>(null)
5658
const [dragActive, setDragActive] = useState(false)
5759
const [dailyUsageData, setDailyUsageData] = useState<DailyUsageData[]>([])
58-
const [activeView, setActiveView] = useState<'overview' | 'users' | 'userDetails' | 'costCenters' | 'orgs' | 'models' | 'guide' | 'faq'>('overview')
60+
const [activeView, setActiveView] = useState<'overview' | 'users' | 'userDetails' | 'costCenters' | 'orgs' | 'models' | 'products' | 'guide' | 'faq'>('overview')
5961
const [userUsage, setUserUsage] = useState<UserUsageResult | null>(null)
6062
const [modelUsage, setModelUsage] = useState<ModelUsageResult | null>(null)
63+
const [productUsage, setProductUsage] = useState<ProductUsageResult | null>(null)
6164
const [selectedUsername, setSelectedUsername] = useState<string>('')
6265
const [costCenters, setCostCenters] = useState<CostCenterResult | null>(null)
6366
const [orgs, setOrgs] = useState<OrganizationResult | null>(null)
@@ -71,6 +74,7 @@ function App() {
7174
setDailyUsageData([])
7275
setUserUsage(null)
7376
setModelUsage(null)
77+
setProductUsage(null)
7478
setSelectedUsername('')
7579
setCostCenters(null)
7680
setOrgs(null)
@@ -82,6 +86,7 @@ function App() {
8286
const contextAggregator = new ReportContextAggregator()
8387
const dailyAggregator = new DailyUsageAggregator()
8488
const modelAggregator = new ModelUsageAggregator()
89+
const productAggregator = new ProductUsageAggregator()
8590
const costCenterAggregator = new CostCenterAggregator()
8691
const orgAggregator = new OrganizationAggregator()
8792
const userAggregator = new UserUsageAggregator()
@@ -91,6 +96,7 @@ function App() {
9196
contextAggregator,
9297
dailyAggregator,
9398
modelAggregator,
99+
productAggregator,
94100
costCenterAggregator,
95101
orgAggregator,
96102
userAggregator,
@@ -102,6 +108,7 @@ function App() {
102108

103109
const nextModelUsage = modelAggregator.result()
104110
setModelUsage(nextModelUsage)
111+
setProductUsage(productAggregator.result())
105112

106113
setCostCenters(costCenterAggregator.result())
107114
setOrgs(orgAggregator.result())
@@ -292,6 +299,17 @@ function App() {
292299
</button>
293300
)}
294301

302+
<button
303+
type="button"
304+
className={`sidebar__item ${activeView === 'products' ? 'sidebar__item--active' : ''}`}
305+
onClick={() => setActiveView('products')}
306+
>
307+
<svg className="sidebar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" focusable="false">
308+
<path d="M3 9h18M3 15h18M9 3v18M15 3v18" />
309+
</svg>
310+
<span className="sidebar__label">Products</span>
311+
</button>
312+
295313
{orgs && orgs.organizations.length > 0 && (
296314
<button
297315
type="button"
@@ -321,6 +339,8 @@ function App() {
321339
</button>
322340
)}
323341

342+
<hr className="sidebar__divider" />
343+
324344
<button
325345
type="button"
326346
className={`sidebar__item ${activeView === 'guide' ? 'sidebar__item--active' : ''}`}
@@ -554,6 +574,10 @@ function App() {
554574
<div className="view-content">
555575
<CostCentersView data={costCenters ?? { costCenters: [] }} rangeStart={rangeStart} />
556576
</div>
577+
) : activeView === 'products' ? (
578+
<div className="view-content">
579+
<ProductsView data={productUsage ?? { products: [] }} />
580+
</div>
557581
) : activeView === 'guide' ? (
558582
<div className="view-content">
559583
<ReportGuideView />

0 commit comments

Comments
 (0)