Time series metrics

This commit is contained in:
Kieran 2022-06-14 11:46:31 +01:00
parent 1907e3261b
commit fc56aa8336
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
20 changed files with 533 additions and 62 deletions

View File

@ -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;
}
/// <summary>
@ -29,9 +32,11 @@ 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<BandwidthPoint> TimeSeriesMetrics);
}

View File

@ -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;
}
/// <summary>
/// Get stats for a specific file
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
[Route("{id}")]
public async Task<FileStats> GetFileStats([FromRoute] string id)
{
var bw = await _statsReporter.GetBandwidth(id.FromBase58Guid());
return new(bw);
}
}
public sealed record FileStats(Bandwidth Bandwidth);
}

View File

@ -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;
}
/// <summary>
@ -163,6 +166,22 @@ namespace VoidCat.Controllers
return isOwner ? Json(await _fileInfo.GetPrivate(fid)) : Json(await _fileInfo.Get(fid));
}
/// <summary>
/// Return information about a specific file
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
[Route("{id}/metrics")]
public async Task<IActionResult> 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);
}
/// <summary>
/// Create a paywall order to pay
/// </summary>

View File

@ -1,3 +1,16 @@
namespace VoidCat.Model;
/// <summary>
/// I/O bandwidth model
/// </summary>
/// <param name="Ingress"></param>
/// <param name="Egress"></param>
public sealed record Bandwidth(ulong Ingress, ulong Egress);
/// <summary>
/// I/O bandwidth model at a specific time
/// </summary>
/// <param name="Time"></param>
/// <param name="Ingress"></param>
/// <param name="Egress"></param>
public sealed record BandwidthPoint(DateTime Time, ulong Ingress, ulong Egress);

View File

@ -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;
}

View File

@ -2,35 +2,79 @@
namespace VoidCat.Model
{
/// <summary>
/// System settings
/// </summary>
public class VoidSettings
{
/// <summary>
/// Data directory to store files in
/// </summary>
public string DataDirectory { get; init; } = "./data";
/// <summary>
/// Tor configuration
/// </summary>
public TorSettings? TorSettings { get; init; }
/// <summary>
/// JWT settings for login token signing
/// </summary>
public JwtSettings JwtSettings { get; init; } = new()
{
Issuer = "void_cat_internal",
Key = "default_key_void_cat_host"
};
/// <summary>
/// Redis database connection string
/// </summary>
public string? Redis { get; init; }
/// <summary>
/// Strike payment service api settings
/// </summary>
public StrikeApiSettings? Strike { get; init; }
/// <summary>
/// Email server settings
/// </summary>
public SmtpSettings? Smtp { get; init; }
/// <summary>
/// CORS origins
/// </summary>
public List<Uri> CorsOrigins { get; init; } = new();
/// <summary>
/// Cloud file storage settings
/// </summary>
public CloudStorageSettings? CloudStorage { get; init; }
/// <summary>
/// Virus scanner settings
/// </summary>
public VirusScannerSettings? VirusScanner { get; init; }
/// <summary>
/// Request header to unmask in the logs, otherwise all are masked
/// </summary>
public IEnumerable<string>? RequestHeadersLog { get; init; }
/// <summary>
/// hCaptcha settings
/// </summary>
public CaptchaSettings? CaptchaSettings { get; init; }
/// <summary>
/// Postgres database connection string
/// </summary>
public string? Postgres { get; init; }
/// <summary>
/// Prometheus server for querying metrics
/// </summary>
public Uri? Prometheus { get; init; }
}
public sealed class TorSettings

View File

@ -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<IMigration, MigrateToPostgres>();
services.AddStorage(voidSettings);
// stats
services.AddTransient<IAggregateStatsCollector, AggregateStatsCollector>();
services.AddTransient<IStatsCollector, PrometheusStatsCollector>();
services.AddMetrics(voidSettings);
// paywall
services.AddPaywallServices(voidSettings);
@ -171,12 +169,9 @@ if (!string.IsNullOrEmpty(voidSettings.Postgres))
.ScanIn(typeof(Program).Assembly).For.Migrations());
}
if (useRedis)
if (voidSettings.HasRedis())
{
services.AddTransient<ICache, RedisCache>();
services.AddTransient<RedisStatsController>();
services.AddTransient<IStatsCollector>(svc => svc.GetRequiredService<RedisStatsController>());
services.AddTransient<IStatsReporter>(svc => svc.GetRequiredService<RedisStatsController>());
// redis specific migrations
services.AddTransient<IMigration, UserLookupKeyHashMigration>();
@ -185,9 +180,6 @@ else
{
services.AddMemoryCache();
services.AddTransient<ICache, InMemoryCache>();
services.AddTransient<InMemoryStatsController>();
services.AddTransient<IStatsReporter>(svc => svc.GetRequiredService<InMemoryStatsController>());
services.AddTransient<IStatsCollector>(svc => svc.GetRequiredService<InMemoryStatsController>());
}
var app = builder.Build();

View File

@ -2,9 +2,28 @@ using VoidCat.Model;
namespace VoidCat.Services.Abstractions;
/// <summary>
/// Get metrics from the system
/// </summary>
public interface IStatsReporter
{
/// <summary>
/// Get global total bandwidth
/// </summary>
/// <returns></returns>
ValueTask<Bandwidth> GetBandwidth();
/// <summary>
/// Get global bandwidth for a single file
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask<Bandwidth> GetBandwidth(Guid id);
/// <summary>
/// Delete bandwidth data for a single file
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
ValueTask Delete(Guid id);
}

View File

@ -0,0 +1,9 @@
using VoidCat.Model;
namespace VoidCat.Services.Abstractions;
public interface ITimeSeriesStatsReporter
{
ValueTask<IReadOnlyList<BandwidthPoint>> GetBandwidth(DateTime start, DateTime end);
ValueTask<IReadOnlyList<BandwidthPoint>> GetBandwidth(Guid id, DateTime start, DateTime end);
}

View File

@ -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)

View File

@ -14,6 +14,7 @@ public class InMemoryStatsController : IStatsCollector, IStatsReporter
_cache = cache;
}
/// <inheritdoc />
public ValueTask TrackIngress(Guid id, ulong amount)
{
Incr(IngressKey(id), amount);
@ -21,6 +22,7 @@ public class InMemoryStatsController : IStatsCollector, IStatsReporter
return ValueTask.CompletedTask;
}
/// <inheritdoc />
public ValueTask TrackEgress(Guid id, ulong amount)
{
Incr(EgressKey(id), amount);
@ -28,12 +30,27 @@ public class InMemoryStatsController : IStatsCollector, IStatsReporter
return ValueTask.CompletedTask;
}
/// <inheritdoc />
public ValueTask<Bandwidth> GetBandwidth()
=> ValueTask.FromResult(GetBandwidthInternal(Global));
/// <inheritdoc />
public ValueTask<IReadOnlyList<BandwidthPoint>> GetBandwidth(DateTime start, DateTime end)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public ValueTask<Bandwidth> GetBandwidth(Guid id)
=> ValueTask.FromResult(GetBandwidthInternal(id));
/// <inheritdoc />
public ValueTask<IReadOnlyList<BandwidthPoint>> GetBandwidth(Guid id, DateTime start, DateTime end)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public ValueTask Delete(Guid id)
{
_cache.Remove(EgressKey(id));

View File

@ -15,6 +15,7 @@ public class RedisStatsController : IStatsReporter, IStatsCollector
_redis = redis;
}
/// <inheritdoc />
public async ValueTask<Bandwidth> GetBandwidth()
{
var egress = _redis.StringGetAsync(GlobalEgress);
@ -24,6 +25,7 @@ public class RedisStatsController : IStatsReporter, IStatsCollector
return new((ulong) ingress.Result, (ulong) egress.Result);
}
/// <inheritdoc />
public async ValueTask<Bandwidth> GetBandwidth(Guid id)
{
var egress = _redis.StringGetAsync(formatEgressKey(id));
@ -33,12 +35,14 @@ public class RedisStatsController : IStatsReporter, IStatsCollector
return new((ulong) ingress.Result, (ulong) egress.Result);
}
/// <inheritdoc />
public async ValueTask Delete(Guid id)
{
await _redis.KeyDeleteAsync(formatEgressKey(id));
await _redis.KeyDeleteAsync(formatIngressKey(id));
}
/// <inheritdoc />
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));
}
/// <inheritdoc />
public async ValueTask TrackEgress(Guid id, ulong amount)
{
await Task.WhenAll(

View File

@ -0,0 +1,22 @@
using VoidCat.Model;
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Stats;
/// <summary>
/// Empty time series reporter
/// </summary>
public class NoTimeSeriesStatsReporter : ITimeSeriesStatsReporter
{
/// <inheritdoc />
public ValueTask<IReadOnlyList<BandwidthPoint>> GetBandwidth(DateTime start, DateTime end)
{
return ValueTask.FromResult<IReadOnlyList<BandwidthPoint>>(new List<BandwidthPoint>());
}
/// <inheritdoc />
public ValueTask<IReadOnlyList<BandwidthPoint>> GetBandwidth(Guid id, DateTime start, DateTime end)
{
return ValueTask.FromResult<IReadOnlyList<BandwidthPoint>>(new List<BandwidthPoint>());
}
}

View File

@ -3,6 +3,7 @@ using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Stats;
/// <inheritdoc />
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");
/// <inheritdoc />
public ValueTask TrackIngress(Guid id, ulong amount)
{
_ingress.Inc(amount);
@ -18,6 +20,7 @@ public class PrometheusStatsCollector : IStatsCollector
return ValueTask.CompletedTask;
}
/// <inheritdoc />
public ValueTask TrackEgress(Guid id, ulong amount)
{
_egress.Inc(amount);

View File

@ -0,0 +1,103 @@
using Newtonsoft.Json;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Stats;
/// <summary>
/// Fetch stats from Prometheus
/// </summary>
public class PrometheusStatsReporter : ITimeSeriesStatsReporter
{
private readonly ILogger<PrometheusStatsReporter> _logger;
private readonly HttpClient _client;
public PrometheusStatsReporter(ILogger<PrometheusStatsReporter> logger, HttpClient client, VoidSettings settings)
{
_client = client;
_logger = logger;
_client.BaseAddress = settings.Prometheus;
}
public async ValueTask<IReadOnlyList<BandwidthPoint>> GetBandwidth(DateTime start, DateTime end)
{
var q = "increase(egress{file=\"\"}[1d])";
return await QueryInner(q, start, end);
}
public async ValueTask<IReadOnlyList<BandwidthPoint>> GetBandwidth(Guid id, DateTime start, DateTime end)
{
var q = $"increase(egress{{file=\"{id}\"}}[1d])";
return await QueryInner(q, start, end);
}
private async Task<IReadOnlyList<BandwidthPoint>> QueryInner(string query, DateTime start, DateTime end)
{
var res = await QueryRange(query, start, end, TimeSpan.FromHours(24));
var bp = new List<BandwidthPoint>();
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<Metrics?> 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<Metrics>(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> 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<List<object>> Values { get; set; }
}
}
}

View File

@ -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<IAggregateStatsCollector, AggregateStatsCollector>();
services.AddTransient<IStatsCollector, PrometheusStatsCollector>();
if (settings.HasPrometheus())
{
services.AddTransient<ITimeSeriesStatsReporter, PrometheusStatsReporter>();
}
else
{
services.AddTransient<ITimeSeriesStatsReporter, NoTimeSeriesStatsReporter>();
}
if (settings.HasRedis())
{
services.AddTransient<RedisStatsController>();
services.AddTransient<IStatsReporter>(svc => svc.GetRequiredService<RedisStatsController>());
services.AddTransient<IStatsCollector>(svc => svc.GetRequiredService<RedisStatsController>());
}
else
{
services.AddTransient<InMemoryStatsController>();
services.AddTransient<IStatsReporter>(svc => svc.GetRequiredService<InMemoryStatsController>());
services.AddTransient<IStatsCollector>(svc => svc.GetRequiredService<InMemoryStatsController>());
}
}
}

View File

@ -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",

View File

@ -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 (
<div className="page">
<Dropzone/>
<GlobalStats/>
<MetricsGraph metrics={metrics}/>
<FooterLinks/>
</div>
);

View File

@ -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 (
<BarChart
width={720}
height={200}
data={metrics.timeSeriesMetrics}
margin={{left: 0, right: 0}}
style={{userSelect: "none"}}>
<XAxis dataKey="time" tickFormatter={(v, i) => `${moment(v).format("DD-MMM")}`}/>
<Bar dataKey="egress" fill="#ccc"/>
<Tooltip formatter={(v) => FormatBytes(v, 2)} labelStyle={{color: "#aaa"}} itemStyle={{color: "#eee"}}
contentStyle={{backgroundColor: "#111"}}/>
</BarChart>
);
}

View File

@ -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"