File size: 3,251 Bytes
4fc4790
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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)
    }
}