mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-04-09 22:29:03 +02:00
Time series metrics
This commit is contained in:
parent
1907e3261b
commit
fc56aa8336
@ -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>
|
||||
@ -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<BandwidthPoint> TimeSeriesMetrics);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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>
|
||||
|
@ -1,3 +1,16 @@
|
||||
namespace VoidCat.Model;
|
||||
|
||||
public sealed record Bandwidth(ulong Ingress, ulong Egress);
|
||||
/// <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);
|
@ -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;
|
||||
}
|
@ -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
|
||||
|
@ -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,13 +169,10 @@ 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();
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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)
|
||||
|
@ -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));
|
||||
@ -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}";
|
||||
}
|
||||
}
|
@ -15,30 +15,34 @@ public class RedisStatsController : IStatsReporter, IStatsCollector
|
||||
_redis = redis;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<Bandwidth> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<Bandwidth> 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);
|
||||
}
|
||||
|
||||
/// <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(
|
||||
@ -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";
|
||||
}
|
||||
}
|
22
VoidCat/Services/Stats/NoTimeSeriesStatsReporter.cs
Normal file
22
VoidCat/Services/Stats/NoTimeSeriesStatsReporter.cs
Normal 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>());
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
103
VoidCat/Services/Stats/PrometheusStatsReporter.cs
Normal file
103
VoidCat/Services/Stats/PrometheusStatsReporter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
}
|
37
VoidCat/Services/Stats/StatsStartup.cs
Normal file
37
VoidCat/Services/Stats/StatsStartup.cs
Normal 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>());
|
||||
}
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
|
23
VoidCat/spa/src/MetricsGraph.js
Normal file
23
VoidCat/spa/src/MetricsGraph.js
Normal 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>
|
||||
);
|
||||
}
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user