diff --git a/VoidCat/Controllers/InfoController.cs b/VoidCat/Controllers/InfoController.cs index 11e31e0..961e33a 100644 --- a/VoidCat/Controllers/InfoController.cs +++ b/VoidCat/Controllers/InfoController.cs @@ -10,12 +10,15 @@ public class InfoController : Controller private readonly IStatsReporter _statsReporter; private readonly IFileMetadataStore _fileMetadata; private readonly VoidSettings _settings; + private readonly ITimeSeriesStatsReporter _timeSeriesStats; - public InfoController(IStatsReporter statsReporter, IFileMetadataStore fileMetadata, VoidSettings settings) + public InfoController(IStatsReporter statsReporter, IFileMetadataStore fileMetadata, VoidSettings settings, + ITimeSeriesStatsReporter stats) { _statsReporter = statsReporter; _fileMetadata = fileMetadata; _settings = settings; + _timeSeriesStats = stats; } /// @@ -28,10 +31,12 @@ public class InfoController : Controller { var bw = await _statsReporter.GetBandwidth(); var storeStats = await _fileMetadata.Stats(); - - return new(bw, (ulong)storeStats.Size, storeStats.Files, BuildInfo.GetBuildInfo(), _settings.CaptchaSettings?.SiteKey); + + return new(bw, storeStats.Size, storeStats.Files, BuildInfo.GetBuildInfo(), + _settings.CaptchaSettings?.SiteKey, + await _timeSeriesStats.GetBandwidth(DateTime.UtcNow.AddDays(-30), DateTime.UtcNow)); } public sealed record GlobalInfo(Bandwidth Bandwidth, ulong TotalBytes, long Count, BuildInfo BuildInfo, - string? CaptchaSiteKey); + string? CaptchaSiteKey, IEnumerable TimeSeriesMetrics); } \ No newline at end of file diff --git a/VoidCat/Controllers/StatsController.cs b/VoidCat/Controllers/StatsController.cs deleted file mode 100644 index 359cf4f..0000000 --- a/VoidCat/Controllers/StatsController.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using VoidCat.Model; -using VoidCat.Services.Abstractions; - -namespace VoidCat.Controllers -{ - [Route("stats")] - public class StatsController : Controller - { - private readonly IStatsReporter _statsReporter; - private readonly IFileStore _fileStore; - - public StatsController(IStatsReporter statsReporter, IFileStore fileStore) - { - _statsReporter = statsReporter; - _fileStore = fileStore; - } - - /// - /// Get stats for a specific file - /// - /// - /// - [HttpGet] - [Route("{id}")] - public async Task GetFileStats([FromRoute] string id) - { - var bw = await _statsReporter.GetBandwidth(id.FromBase58Guid()); - return new(bw); - } - } - - public sealed record FileStats(Bandwidth Bandwidth); -} \ No newline at end of file diff --git a/VoidCat/Controllers/UploadController.cs b/VoidCat/Controllers/UploadController.cs index fb87ac5..9576104 100644 --- a/VoidCat/Controllers/UploadController.cs +++ b/VoidCat/Controllers/UploadController.cs @@ -18,9 +18,11 @@ namespace VoidCat.Controllers private readonly IPaywallFactory _paywallFactory; private readonly IFileInfoManager _fileInfo; private readonly IUserUploadsStore _userUploads; + private readonly ITimeSeriesStatsReporter _timeSeriesStats; public UploadController(IFileStore storage, IFileMetadataStore metadata, IPaywallStore paywall, - IPaywallFactory paywallFactory, IFileInfoManager fileInfo, IUserUploadsStore userUploads) + IPaywallFactory paywallFactory, IFileInfoManager fileInfo, IUserUploadsStore userUploads, + ITimeSeriesStatsReporter timeSeriesStats) { _storage = storage; _metadata = metadata; @@ -28,6 +30,7 @@ namespace VoidCat.Controllers _paywallFactory = paywallFactory; _fileInfo = fileInfo; _userUploads = userUploads; + _timeSeriesStats = timeSeriesStats; } /// @@ -163,6 +166,22 @@ namespace VoidCat.Controllers return isOwner ? Json(await _fileInfo.GetPrivate(fid)) : Json(await _fileInfo.Get(fid)); } + /// + /// Return information about a specific file + /// + /// + /// + [HttpGet] + [Route("{id}/metrics")] + public async Task Metrics([FromRoute] string id) + { + if (!id.TryFromBase58Guid(out var fid)) return StatusCode(404); + + var stats = await _timeSeriesStats.GetBandwidth(Guid.Parse("0327ed25-69cb-489a-ae37-2e512a63e4a4"), DateTime.UtcNow.Subtract(TimeSpan.FromDays(30)), DateTime.UtcNow); + + return Json(stats); + } + /// /// Create a paywall order to pay /// diff --git a/VoidCat/Model/Bandwidth.cs b/VoidCat/Model/Bandwidth.cs index d700f98..b28a6d1 100644 --- a/VoidCat/Model/Bandwidth.cs +++ b/VoidCat/Model/Bandwidth.cs @@ -1,3 +1,16 @@ namespace VoidCat.Model; -public sealed record Bandwidth(ulong Ingress, ulong Egress); \ No newline at end of file +/// +/// I/O bandwidth model +/// +/// +/// +public sealed record Bandwidth(ulong Ingress, ulong Egress); + +/// +/// I/O bandwidth model at a specific time +/// +/// +/// +/// +public sealed record BandwidthPoint(DateTime Time, ulong Ingress, ulong Egress); \ No newline at end of file diff --git a/VoidCat/Model/Extensions.cs b/VoidCat/Model/Extensions.cs index 46f8dbf..20a5688 100644 --- a/VoidCat/Model/Extensions.cs +++ b/VoidCat/Model/Extensions.cs @@ -226,4 +226,10 @@ public static class Extensions public static bool HasPostgres(this VoidSettings settings) => !string.IsNullOrEmpty(settings.Postgres); + + public static bool HasRedis(this VoidSettings settings) + => !string.IsNullOrEmpty(settings.Redis); + + public static bool HasPrometheus(this VoidSettings settings) + => settings.Prometheus != null; } \ No newline at end of file diff --git a/VoidCat/Model/VoidSettings.cs b/VoidCat/Model/VoidSettings.cs index 2561982..233a5c8 100644 --- a/VoidCat/Model/VoidSettings.cs +++ b/VoidCat/Model/VoidSettings.cs @@ -2,35 +2,79 @@ namespace VoidCat.Model { + /// + /// System settings + /// public class VoidSettings { + /// + /// Data directory to store files in + /// public string DataDirectory { get; init; } = "./data"; + /// + /// Tor configuration + /// public TorSettings? TorSettings { get; init; } + /// + /// JWT settings for login token signing + /// public JwtSettings JwtSettings { get; init; } = new() { Issuer = "void_cat_internal", Key = "default_key_void_cat_host" }; + /// + /// Redis database connection string + /// public string? Redis { get; init; } + /// + /// Strike payment service api settings + /// public StrikeApiSettings? Strike { get; init; } + /// + /// Email server settings + /// public SmtpSettings? Smtp { get; init; } + /// + /// CORS origins + /// public List CorsOrigins { get; init; } = new(); + /// + /// Cloud file storage settings + /// public CloudStorageSettings? CloudStorage { get; init; } + /// + /// Virus scanner settings + /// public VirusScannerSettings? VirusScanner { get; init; } + /// + /// Request header to unmask in the logs, otherwise all are masked + /// public IEnumerable? RequestHeadersLog { get; init; } + /// + /// hCaptcha settings + /// public CaptchaSettings? CaptchaSettings { get; init; } + /// + /// Postgres database connection string + /// public string? Postgres { get; init; } + + /// + /// Prometheus server for querying metrics + /// + public Uri? Prometheus { get; init; } } public sealed class TorSettings diff --git a/VoidCat/Program.cs b/VoidCat/Program.cs index e51a680..a913240 100644 --- a/VoidCat/Program.cs +++ b/VoidCat/Program.cs @@ -46,8 +46,7 @@ services.AddSingleton(voidSettings.Strike ?? new()); var seqSettings = configuration.GetSection("Seq"); builder.Logging.AddSeq(seqSettings); -var useRedis = !string.IsNullOrEmpty(voidSettings.Redis); -if (useRedis) +if (voidSettings.HasRedis()) { var cx = await ConnectionMultiplexer.ConnectAsync(voidSettings.Redis); services.AddSingleton(cx); @@ -138,8 +137,7 @@ services.AddTransient(); services.AddStorage(voidSettings); // stats -services.AddTransient(); -services.AddTransient(); +services.AddMetrics(voidSettings); // paywall services.AddPaywallServices(voidSettings); @@ -171,13 +169,10 @@ if (!string.IsNullOrEmpty(voidSettings.Postgres)) .ScanIn(typeof(Program).Assembly).For.Migrations()); } -if (useRedis) +if (voidSettings.HasRedis()) { services.AddTransient(); - services.AddTransient(); - services.AddTransient(svc => svc.GetRequiredService()); - services.AddTransient(svc => svc.GetRequiredService()); - + // redis specific migrations services.AddTransient(); } @@ -185,9 +180,6 @@ else { services.AddMemoryCache(); services.AddTransient(); - services.AddTransient(); - services.AddTransient(svc => svc.GetRequiredService()); - services.AddTransient(svc => svc.GetRequiredService()); } var app = builder.Build(); diff --git a/VoidCat/Services/Abstractions/IStatsReporter.cs b/VoidCat/Services/Abstractions/IStatsReporter.cs index e2612aa..0d19469 100644 --- a/VoidCat/Services/Abstractions/IStatsReporter.cs +++ b/VoidCat/Services/Abstractions/IStatsReporter.cs @@ -2,9 +2,28 @@ using VoidCat.Model; namespace VoidCat.Services.Abstractions; +/// +/// Get metrics from the system +/// public interface IStatsReporter { + /// + /// Get global total bandwidth + /// + /// ValueTask GetBandwidth(); + + /// + /// Get global bandwidth for a single file + /// + /// + /// ValueTask GetBandwidth(Guid id); + + /// + /// Delete bandwidth data for a single file + /// + /// + /// ValueTask Delete(Guid id); } \ No newline at end of file diff --git a/VoidCat/Services/Abstractions/ITimeSeriesStatsReporter.cs b/VoidCat/Services/Abstractions/ITimeSeriesStatsReporter.cs new file mode 100644 index 0000000..b21aa12 --- /dev/null +++ b/VoidCat/Services/Abstractions/ITimeSeriesStatsReporter.cs @@ -0,0 +1,9 @@ +using VoidCat.Model; + +namespace VoidCat.Services.Abstractions; + +public interface ITimeSeriesStatsReporter +{ + ValueTask> GetBandwidth(DateTime start, DateTime end); + ValueTask> GetBandwidth(Guid id, DateTime start, DateTime end); +} \ No newline at end of file diff --git a/VoidCat/Services/Background/VirusScannerService.cs b/VoidCat/Services/Background/VirusScannerService.cs index f0fd93a..214ce78 100644 --- a/VoidCat/Services/Background/VirusScannerService.cs +++ b/VoidCat/Services/Background/VirusScannerService.cs @@ -35,6 +35,9 @@ public class VirusScannerService : BackgroundService await foreach (var file in files.Results.WithCancellation(stoppingToken)) { + // file is too large, cant scan + if (file.Size > 4_000_000) continue; + // check for scans var scan = await _scanStore.GetByFile(file.Id); if (scan == default) diff --git a/VoidCat/Services/InMemory/InMemoryStatsController.cs b/VoidCat/Services/InMemory/InMemoryStatsController.cs index e99d729..3af5e07 100644 --- a/VoidCat/Services/InMemory/InMemoryStatsController.cs +++ b/VoidCat/Services/InMemory/InMemoryStatsController.cs @@ -14,6 +14,7 @@ public class InMemoryStatsController : IStatsCollector, IStatsReporter _cache = cache; } + /// public ValueTask TrackIngress(Guid id, ulong amount) { Incr(IngressKey(id), amount); @@ -21,6 +22,7 @@ public class InMemoryStatsController : IStatsCollector, IStatsReporter return ValueTask.CompletedTask; } + /// public ValueTask TrackEgress(Guid id, ulong amount) { Incr(EgressKey(id), amount); @@ -28,12 +30,27 @@ public class InMemoryStatsController : IStatsCollector, IStatsReporter return ValueTask.CompletedTask; } + /// public ValueTask GetBandwidth() => ValueTask.FromResult(GetBandwidthInternal(Global)); + /// + public ValueTask> GetBandwidth(DateTime start, DateTime end) + { + throw new NotImplementedException(); + } + + /// public ValueTask GetBandwidth(Guid id) => ValueTask.FromResult(GetBandwidthInternal(id)); + /// + public ValueTask> GetBandwidth(Guid id, DateTime start, DateTime end) + { + throw new NotImplementedException(); + } + + /// public ValueTask Delete(Guid id) { _cache.Remove(EgressKey(id)); @@ -56,4 +73,4 @@ public class InMemoryStatsController : IStatsCollector, IStatsReporter private string IngressKey(Guid id) => $"stats:ingress:{id}"; private string EgressKey(Guid id) => $"stats:egress:{id}"; -} +} \ No newline at end of file diff --git a/VoidCat/Services/Redis/RedisStatsController.cs b/VoidCat/Services/Redis/RedisStatsController.cs index 2a41aa8..cebb4e0 100644 --- a/VoidCat/Services/Redis/RedisStatsController.cs +++ b/VoidCat/Services/Redis/RedisStatsController.cs @@ -15,30 +15,34 @@ public class RedisStatsController : IStatsReporter, IStatsCollector _redis = redis; } + /// public async ValueTask GetBandwidth() { var egress = _redis.StringGetAsync(GlobalEgress); var ingress = _redis.StringGetAsync(GlobalIngress); await Task.WhenAll(egress, ingress); - return new((ulong)ingress.Result, (ulong)egress.Result); + return new((ulong) ingress.Result, (ulong) egress.Result); } + /// public async ValueTask GetBandwidth(Guid id) { var egress = _redis.StringGetAsync(formatEgressKey(id)); var ingress = _redis.StringGetAsync(formatIngressKey(id)); await Task.WhenAll(egress, ingress); - return new((ulong)ingress.Result, (ulong)egress.Result); + return new((ulong) ingress.Result, (ulong) egress.Result); } + /// public async ValueTask Delete(Guid id) { await _redis.KeyDeleteAsync(formatEgressKey(id)); await _redis.KeyDeleteAsync(formatIngressKey(id)); } + /// public async ValueTask TrackIngress(Guid id, ulong amount) { await Task.WhenAll( @@ -46,6 +50,7 @@ public class RedisStatsController : IStatsReporter, IStatsCollector _redis.StringIncrementAsync(formatIngressKey(id), amount)); } + /// public async ValueTask TrackEgress(Guid id, ulong amount) { await Task.WhenAll( @@ -55,4 +60,4 @@ public class RedisStatsController : IStatsReporter, IStatsCollector private RedisKey formatIngressKey(Guid id) => $"stats:{id}:ingress"; private RedisKey formatEgressKey(Guid id) => $"stats:{id}:egress"; -} +} \ No newline at end of file diff --git a/VoidCat/Services/Stats/NoTimeSeriesStatsReporter.cs b/VoidCat/Services/Stats/NoTimeSeriesStatsReporter.cs new file mode 100644 index 0000000..b19f5f8 --- /dev/null +++ b/VoidCat/Services/Stats/NoTimeSeriesStatsReporter.cs @@ -0,0 +1,22 @@ +using VoidCat.Model; +using VoidCat.Services.Abstractions; + +namespace VoidCat.Services.Stats; + +/// +/// Empty time series reporter +/// +public class NoTimeSeriesStatsReporter : ITimeSeriesStatsReporter +{ + /// + public ValueTask> GetBandwidth(DateTime start, DateTime end) + { + return ValueTask.FromResult>(new List()); + } + + /// + public ValueTask> GetBandwidth(Guid id, DateTime start, DateTime end) + { + return ValueTask.FromResult>(new List()); + } +} \ No newline at end of file diff --git a/VoidCat/Services/Stats/PrometheusStatsCollector.cs b/VoidCat/Services/Stats/PrometheusStatsCollector.cs index 444e376..3b6e6e5 100644 --- a/VoidCat/Services/Stats/PrometheusStatsCollector.cs +++ b/VoidCat/Services/Stats/PrometheusStatsCollector.cs @@ -3,6 +3,7 @@ using VoidCat.Services.Abstractions; namespace VoidCat.Services.Stats; +/// public class PrometheusStatsCollector : IStatsCollector { private readonly Counter _egress = @@ -11,6 +12,7 @@ public class PrometheusStatsCollector : IStatsCollector private readonly Counter _ingress = Metrics.CreateCounter("ingress", "Incoming traffic to the site", "file"); + /// public ValueTask TrackIngress(Guid id, ulong amount) { _ingress.Inc(amount); @@ -18,6 +20,7 @@ public class PrometheusStatsCollector : IStatsCollector return ValueTask.CompletedTask; } + /// public ValueTask TrackEgress(Guid id, ulong amount) { _egress.Inc(amount); diff --git a/VoidCat/Services/Stats/PrometheusStatsReporter.cs b/VoidCat/Services/Stats/PrometheusStatsReporter.cs new file mode 100644 index 0000000..a61451e --- /dev/null +++ b/VoidCat/Services/Stats/PrometheusStatsReporter.cs @@ -0,0 +1,103 @@ +using Newtonsoft.Json; +using VoidCat.Model; +using VoidCat.Services.Abstractions; + +namespace VoidCat.Services.Stats; + +/// +/// Fetch stats from Prometheus +/// +public class PrometheusStatsReporter : ITimeSeriesStatsReporter +{ + private readonly ILogger _logger; + private readonly HttpClient _client; + + public PrometheusStatsReporter(ILogger logger, HttpClient client, VoidSettings settings) + { + _client = client; + _logger = logger; + + _client.BaseAddress = settings.Prometheus; + } + + public async ValueTask> GetBandwidth(DateTime start, DateTime end) + { + var q = "increase(egress{file=\"\"}[1d])"; + return await QueryInner(q, start, end); + } + + public async ValueTask> GetBandwidth(Guid id, DateTime start, DateTime end) + { + var q = $"increase(egress{{file=\"{id}\"}}[1d])"; + return await QueryInner(q, start, end); + } + + private async Task> QueryInner(string query, DateTime start, DateTime end) + { + var res = await QueryRange(query, start, end, TimeSpan.FromHours(24)); + + var bp = new List(); + foreach (var r in res.Data.Result) + { + foreach (var v in r.Values) + { + bp.Add(new(DateTimeOffset.FromUnixTimeSeconds((long) v[0]) + .DateTime, 0ul, + (ulong) decimal.Parse(v[1] as string ?? "0"))); + } + } + + return bp; + } + + private async Task QueryRange(string query, DateTimeOffset start, DateTimeOffset end, TimeSpan step) + { + var url = + $"/api/v1/query_range?query={Uri.EscapeDataString(query)}&start={start.ToUnixTimeSeconds()}&end={end.ToUnixTimeSeconds()}&step={(int) step.TotalSeconds}"; + var req = await _client.SendAsync(new(HttpMethod.Get, url)); + if (req.IsSuccessStatusCode) + { + var json = await req.Content.ReadAsStringAsync(); + if (!string.IsNullOrEmpty(json)) + { + return JsonConvert.DeserializeObject(json); + } + } + else + { + _logger.LogWarning("Failed to fetch metrics: {Url} {Status}", url, req.StatusCode); + } + + return default; + } + + private class Metrics + { + [JsonProperty("status")] public string Status { get; set; } + + [JsonProperty("data")] public MetricData Data { get; set; } + + public class MetricData + { + [JsonProperty("resultType")] public string ResultType { get; set; } + + [JsonProperty("result")] public List Result { get; set; } + } + + public class Metric + { + [JsonProperty("file")] public string File { get; set; } + + [JsonProperty("instance")] public string Instance { get; set; } + + [JsonProperty("job")] public string Job { get; set; } + } + + public class Result + { + [JsonProperty("metric")] public Metric Metric { get; set; } + + [JsonProperty("values")] public List> Values { get; set; } + } + } +} \ No newline at end of file diff --git a/VoidCat/Services/Stats/StatsStartup.cs b/VoidCat/Services/Stats/StatsStartup.cs new file mode 100644 index 0000000..0858a4f --- /dev/null +++ b/VoidCat/Services/Stats/StatsStartup.cs @@ -0,0 +1,37 @@ +using VoidCat.Model; +using VoidCat.Services.Abstractions; +using VoidCat.Services.InMemory; +using VoidCat.Services.Redis; + +namespace VoidCat.Services.Stats; + +public static class StatsStartup +{ + public static void AddMetrics(this IServiceCollection services, VoidSettings settings) + { + services.AddTransient(); + services.AddTransient(); + + if (settings.HasPrometheus()) + { + services.AddTransient(); + } + else + { + services.AddTransient(); + } + + if (settings.HasRedis()) + { + services.AddTransient(); + services.AddTransient(svc => svc.GetRequiredService()); + services.AddTransient(svc => svc.GetRequiredService()); + } + else + { + services.AddTransient(); + services.AddTransient(svc => svc.GetRequiredService()); + services.AddTransient(svc => svc.GetRequiredService()); + } + } +} \ No newline at end of file diff --git a/VoidCat/spa/package.json b/VoidCat/spa/package.json index 79c6afa..840f182 100644 --- a/VoidCat/spa/package.json +++ b/VoidCat/spa/package.json @@ -15,7 +15,8 @@ "react-helmet": "^6.1.0", "react-redux": "^7.2.6", "react-router-dom": "^6.2.1", - "react-scripts": "5.0.0" + "react-scripts": "5.0.0", + "recharts": "^2.1.10" }, "scripts": { "start": "react-scripts start", diff --git a/VoidCat/spa/src/HomePage.js b/VoidCat/spa/src/HomePage.js index 0023b39..7a984e6 100644 --- a/VoidCat/spa/src/HomePage.js +++ b/VoidCat/spa/src/HomePage.js @@ -1,12 +1,16 @@ import {Dropzone} from "./Dropzone"; import {GlobalStats} from "./GlobalStats"; import {FooterLinks} from "./FooterLinks"; +import {MetricsGraph} from "./MetricsGraph"; +import {useSelector} from "react-redux"; export function HomePage() { + const metrics = useSelector(a => a.info.stats); return (
+
); diff --git a/VoidCat/spa/src/MetricsGraph.js b/VoidCat/spa/src/MetricsGraph.js new file mode 100644 index 0000000..9b7664c --- /dev/null +++ b/VoidCat/spa/src/MetricsGraph.js @@ -0,0 +1,23 @@ +import {Bar, BarChart, Tooltip, XAxis} from "recharts"; +import {FormatBytes} from "./Util"; +import moment from "moment"; + +export function MetricsGraph(props) { + const metrics = props.metrics; + + if (!metrics?.timeSeriesMetrics || metrics?.timeSeriesMetrics.length === 0) return null; + + return ( + + `${moment(v).format("DD-MMM")}`}/> + + FormatBytes(v, 2)} labelStyle={{color: "#aaa"}} itemStyle={{color: "#eee"}} + contentStyle={{backgroundColor: "#111"}}/> + + ); +} \ No newline at end of file diff --git a/VoidCat/spa/yarn.lock b/VoidCat/spa/yarn.lock index 338547f..4f99967 100644 --- a/VoidCat/spa/yarn.lock +++ b/VoidCat/spa/yarn.lock @@ -1015,6 +1015,13 @@ core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" +"@babel/runtime@^7.1.2": + version "7.18.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4" + integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" @@ -1747,6 +1754,11 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/resize-observer-browser@^0.1.6": + version "0.1.7" + resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz#294aaadf24ac6580b8fbd1fe3ab7b59fe85f9ef3" + integrity sha512-G9eN0Sn0ii9PWQ3Vl72jDPgeJwRWhv2Qk/nQkJuWmRmOB4HX3/BhD5SE1dZs/hzPZL/WKnvF0RHdTSG54QJFyg== + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -2736,6 +2748,11 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== +classnames@^2.2.5: + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== + clean-css@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.2.2.tgz#d3a7c6ee2511011e051719838bdcf8314dc4548d" @@ -3064,6 +3081,11 @@ css-tree@^1.1.2, css-tree@^1.1.3: mdn-data "2.0.14" source-map "^0.6.1" +css-unit-converter@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21" + integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA== + css-what@^3.2.1: version "3.4.2" resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" @@ -3162,6 +3184,67 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== +d3-array@2, d3-array@^2.3.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" + integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== + dependencies: + internmap "^1.0.0" + +"d3-color@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" + integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== + +"d3-format@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" + integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== + +"d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" + integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== + dependencies: + d3-color "1 - 2" + +"d3-path@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" + integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== + +d3-scale@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3" + integrity sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ== + dependencies: + d3-array "^2.3.0" + d3-format "1 - 2" + d3-interpolate "1.2.0 - 2" + d3-time "^2.1.1" + d3-time-format "2 - 3" + +d3-shape@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-2.1.0.tgz#3b6a82ccafbc45de55b57fcf956c584ded3b666f" + integrity sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA== + dependencies: + d3-path "1 - 2" + +"d3-time-format@2 - 3": + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" + integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== + dependencies: + d3-time "1 - 2" + +"d3-time@1 - 2", d3-time@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682" + integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ== + dependencies: + d3-array "2" + damerau-levenshtein@^1.0.7: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" @@ -3197,6 +3280,11 @@ debug@^3.1.1, debug@^3.2.7: dependencies: ms "^2.1.1" +decimal.js-light@^2.4.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + decimal.js@^10.2.1: version "10.3.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" @@ -3372,6 +3460,13 @@ dom-converter@^0.2.0: dependencies: utila "~0.4" +dom-helpers@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== + dependencies: + "@babel/runtime" "^7.1.2" + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -3857,7 +3952,7 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -eventemitter3@^4.0.0: +eventemitter3@^4.0.0, eventemitter3@^4.0.1: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -3938,6 +4033,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-equals@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-2.0.4.tgz#3add9410585e2d7364c2deeb6a707beadb24b927" + integrity sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w== + fast-glob@^3.2.11, fast-glob@^3.2.9: version "3.2.11" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" @@ -4607,6 +4707,11 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internmap@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" + integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== + ip@^1.1.0: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" @@ -5569,7 +5674,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.14, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -6681,6 +6786,11 @@ postcss-unique-selectors@^5.0.3: dependencies: postcss-selector-parser "^6.0.5" +postcss-value-parser@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" @@ -6762,7 +6872,7 @@ prompts@^2.0.1, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.6.0, prop-types@^15.7.2: +prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -6823,7 +6933,7 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -raf@^3.4.1: +raf@^3.4.0, raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== @@ -6923,7 +7033,7 @@ react-helmet@^6.1.0: react-fast-compare "^3.1.1" react-side-effect "^2.1.0" -react-is@^16.13.1, react-is@^16.7.0: +react-is@^16.10.2, react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -6933,6 +7043,11 @@ react-is@^17.0.1, react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + react-redux@^7.2.6: version "7.2.6" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.6.tgz#49633a24fe552b5f9caf58feb8a138936ddfe9aa" @@ -6950,6 +7065,15 @@ react-refresh@^0.11.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== +react-resize-detector@^6.6.3: + version "6.7.8" + resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-6.7.8.tgz#318c85d1335e50f99d4fb8eb9ec34e066db597d0" + integrity sha512-0FaEcUBAbn+pq3PT5a9hHRebUfuS1SRLGLpIw8LydU7zX429I6XJgKerKAMPsJH0qWAl6o5bVKNqFJqr6tGPYw== + dependencies: + "@types/resize-observer-browser" "^0.1.6" + lodash "^4.17.21" + resize-observer-polyfill "^1.5.1" + react-router-dom@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.2.1.tgz#32ec81829152fbb8a7b045bf593a22eadf019bec" @@ -7025,6 +7149,25 @@ react-side-effect@^2.1.0: resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.1.tgz#66c5701c3e7560ab4822a4ee2742dee215d72eb3" integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ== +react-smooth@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-2.0.0.tgz#561647b33e498b2e25f449b3c6689b2e9111bf91" + integrity sha512-wK4dBBR6P21otowgMT9toZk+GngMplGS1O5gk+2WSiHEXIrQgDvhR5IIlT74Vtu//qpTcipkgo21dD7a7AUNxw== + dependencies: + fast-equals "^2.0.0" + raf "^3.4.0" + react-transition-group "2.9.0" + +react-transition-group@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" + integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== + dependencies: + dom-helpers "^3.4.0" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-lifecycles-compat "^3.0.4" + react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" @@ -7062,6 +7205,30 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +recharts-scale@^0.4.4: + version "0.4.5" + resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" + integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== + dependencies: + decimal.js-light "^2.4.1" + +recharts@^2.1.10: + version "2.1.10" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.1.10.tgz#4253f4354fcb9328a162f66d7c5c8d33ef7741db" + integrity sha512-me6c8m2Gs88X/nuM2gDSTDIhpSLNMbiTrlE4Cu53hjZNegT3g3xLlTrbYSAQuBCFWuWJAZXCmEuMr6AwizLyaA== + dependencies: + classnames "^2.2.5" + d3-interpolate "^2.0.0" + d3-scale "^3.0.0" + d3-shape "^2.0.0" + eventemitter3 "^4.0.1" + lodash "^4.17.19" + react-is "^16.10.2" + react-resize-detector "^6.6.3" + react-smooth "^2.0.0" + recharts-scale "^0.4.4" + reduce-css-calc "^2.1.8" + recursive-readdir@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" @@ -7069,6 +7236,14 @@ recursive-readdir@^2.2.2: dependencies: minimatch "3.0.4" +reduce-css-calc@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03" + integrity sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg== + dependencies: + css-unit-converter "^1.1.1" + postcss-value-parser "^3.3.0" + redux-thunk@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.1.tgz#0dd8042cf47868f4b29699941de03c9301a75714" @@ -7183,6 +7358,11 @@ reselect@^4.1.5: resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.5.tgz#852c361247198da6756d07d9296c2b51eddb79f6" integrity sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ== +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"