Spaces:
Sleeping
Sleeping
| import Charts | |
| import SwiftUI | |
| struct CostUsageHistoryMenuView: View { | |
| let summary: GatewayCostUsageSummary | |
| let width: CGFloat | |
| var body: some View { | |
| VStack(alignment: .leading, spacing: 10) { | |
| self.header | |
| self.chart | |
| self.footer | |
| } | |
| .padding(.horizontal, 12) | |
| .padding(.vertical, 10) | |
| .frame(width: max(1, self.width), alignment: .leading) | |
| } | |
| private var header: some View { | |
| let todayKey = CostUsageMenuDateParser.format(Date()) | |
| let todayEntry = self.summary.daily.first { $0.date == todayKey } | |
| let todayCost = CostUsageFormatting.formatUsd(todayEntry?.totalCost) ?? "n/a" | |
| let totalCost = CostUsageFormatting.formatUsd(self.summary.totals.totalCost) ?? "n/a" | |
| return HStack(alignment: .firstTextBaseline, spacing: 12) { | |
| VStack(alignment: .leading, spacing: 2) { | |
| Text("Today") | |
| .font(.caption2) | |
| .foregroundStyle(.secondary) | |
| Text(todayCost) | |
| .font(.system(size: 14, weight: .semibold)) | |
| } | |
| VStack(alignment: .leading, spacing: 2) { | |
| Text("Last \(self.summary.days)d") | |
| .font(.caption2) | |
| .foregroundStyle(.secondary) | |
| Text(totalCost) | |
| .font(.system(size: 14, weight: .semibold)) | |
| } | |
| Spacer() | |
| } | |
| } | |
| private var chart: some View { | |
| let entries = self.summary.daily.compactMap { entry -> (Date, Double)? in | |
| guard let date = CostUsageMenuDateParser.parse(entry.date) else { return nil } | |
| return (date, entry.totalCost) | |
| } | |
| return Chart(entries, id: \.0) { entry in | |
| BarMark( | |
| x: .value("Day", entry.0), | |
| y: .value("Cost", entry.1)) | |
| .foregroundStyle(Color.accentColor) | |
| .cornerRadius(3) | |
| } | |
| .chartXAxis { | |
| AxisMarks(values: .stride(by: .day, count: 7)) { | |
| AxisGridLine().foregroundStyle(.clear) | |
| AxisValueLabel(format: .dateTime.month().day()) | |
| } | |
| } | |
| .chartYAxis { | |
| AxisMarks(position: .leading) { | |
| AxisGridLine() | |
| AxisValueLabel() | |
| } | |
| } | |
| .frame(height: 110) | |
| } | |
| private var footer: some View { | |
| if self.summary.totals.missingCostEntries == 0 { | |
| return AnyView(EmptyView()) | |
| } | |
| return AnyView( | |
| Text("Partial: \(self.summary.totals.missingCostEntries) entries missing cost") | |
| .font(.caption2) | |
| .foregroundStyle(.secondary)) | |
| } | |
| } | |
| private enum CostUsageMenuDateParser { | |
| static let formatter: DateFormatter = { | |
| let formatter = DateFormatter() | |
| formatter.dateFormat = "yyyy-MM-dd" | |
| formatter.locale = Locale(identifier: "en_US_POSIX") | |
| formatter.timeZone = TimeZone.current | |
| return formatter | |
| }() | |
| static func parse(_ value: String) -> Date? { | |
| self.formatter.date(from: value) | |
| } | |
| static func format(_ date: Date) -> String { | |
| self.formatter.string(from: date) | |
| } | |
| } | |