diff --git a/Cargo.lock b/Cargo.lock index 26986efc..acc38b6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.66" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] name = "arrayref" @@ -136,9 +136,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.59" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" +checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" dependencies = [ "proc-macro2", "quote", @@ -285,9 +285,9 @@ checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" [[package]] name = "cc" -version = "1.0.77" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" dependencies = [ "jobserver", ] @@ -300,9 +300,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.0.29" +version = "4.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" +checksum = "656ad1e55e23d287773f7d8192c300dc715c3eeded93b3da651d11c42cfd74d2" dependencies = [ "bitflags", "clap_derive", @@ -1329,9 +1329,9 @@ checksum = "df19da1e92fbfec043ca97d622955381b1f3ee72a180ec999912df31b1ccd951" [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown", @@ -1391,9 +1391,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" [[package]] name = "jobserver" @@ -1784,9 +1784,9 @@ checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "openssl" -version = "0.10.44" +version = "0.10.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d971fd5722fec23977260f6e81aa67d2f22cadbdc2aa049f1022d9a3be1566" +checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" dependencies = [ "bitflags", "cfg-if", @@ -1825,9 +1825,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.79" +version = "0.9.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454462c0eced1e97f2ec09036abc8da362e66802f66fd20f86854d9d8cbcbc4" +checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" dependencies = [ "autocfg", "cc", @@ -2150,9 +2150,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.47" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" dependencies = [ "unicode-ident", ] @@ -2175,9 +2175,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] @@ -2469,9 +2469,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23" +checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" dependencies = [ "bitflags", "errno", @@ -2505,9 +2505,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" [[package]] name = "same-file" @@ -2559,18 +2559,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e326c9ec8042f1b5da33252c8a37e9ffbd2c9bef0155215b6e6c80c790e05f91" +checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a3df25b0713732468deadad63ab9da1f1fd75a48a15024b50363f128db627e" +checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8" dependencies = [ "proc-macro2", "quote", @@ -2579,9 +2579,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.89" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" dependencies = [ "itoa", "ryu", @@ -2633,9 +2633,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.14" +version = "0.9.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d232d893b10de3eb7258ff01974d6ee20663d8e833263c99409d4b13a0209da" +checksum = "92b5b431e8907b50339b51223b97d102db8d987ced36f6e4d03621db9316c834" dependencies = [ "indexmap", "itoa", @@ -2822,9 +2822,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", @@ -2917,6 +2917,7 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ + "itoa", "serde", "time-core", "time-macros", @@ -3316,9 +3317,9 @@ checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "unicode-normalization" diff --git a/Cargo.toml b/Cargo.toml index be5e36fc..def99293 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,14 +15,14 @@ edition = "2021" rust-version = "1.65" [workspace.dependencies] -anyhow = "1.0.66" +anyhow = "1.0.68" askalono = "0.4.6" askama = { git = "https://github.com/djc/askama", rev = "eeec6f0654f32270aec4e4a0d0f42e4ad39bc28e" } askama_axum = { git = "https://github.com/djc/askama", rev = "eeec6f0654f32270aec4e4a0d0f42e4ad39bc28e" } -async-trait = "0.1.59" +async-trait = "0.1.60" axum = { version = "0.6.1", features = ["macros"] } bincode = "1.3.3" -clap = { version = "4.0.29", features = ["derive"] } +clap = { version = "4.0.30", features = ["derive"] } clomonitor-core = { path = "../clomonitor-core" } comfy-table = "6.1.3" config = "0.13.3" @@ -40,21 +40,26 @@ metrics = "0.20.1" metrics-exporter-prometheus = "0.11.0" mime = "0.3.16" mockall = "0.11.3" -openssl = { version = "0.10.44", features = ["vendored"] } +openssl = { version = "0.10.45", features = ["vendored"] } postgres-openssl = "0.5.0" postgres-types = { version = "0.2.4", features = ["derive"] } predicates = "2.1.4" regex = "1.7.0" reqwest = "0.11.13" resvg = "0.27.0" -serde = { version = "1.0.150", features = ["derive"] } -serde_json = "1.0.89" -serde_yaml = "0.9.14" +serde = { version = "1.0.151", features = ["derive"] } +serde_json = "1.0.91" +serde_yaml = "0.9.16" serde_qs = "0.10.1" sha2 = "0.10.6" tempfile = "3.3.0" tera = { version = "1.17.1", default-features = false } -time = { version = "0.3.17", features = ["macros", "parsing", "serde"] } +time = { version = "0.3.17", features = [ + "formatting", + "macros", + "parsing", + "serde", +] } tiny-skia = "0.8.2" tokio = { version = "1.23.0", features = [ "macros", diff --git a/clomonitor-apiserver/src/db.rs b/clomonitor-apiserver/src/db.rs index f4fb8415..c7959d0f 100644 --- a/clomonitor-apiserver/src/db.rs +++ b/clomonitor-apiserver/src/db.rs @@ -1,4 +1,7 @@ -use crate::handlers::RepositoryReportMDTemplate; +use crate::{ + handlers::RepositoryReportMDTemplate, + views::{ProjectId, Total}, +}; use anyhow::Result; use async_trait::async_trait; use clomonitor_core::score::Score; @@ -10,6 +13,9 @@ use std::sync::Arc; use time::Date; use tokio_postgres::types::Json; +// Lock key used when updating the projects views in the database. +const LOCK_KEY_UPDATE_PROJECTS_VIEWS: i64 = 1; + /// Type alias to represent a DB trait object. pub(crate) type DynDB = Arc; @@ -59,7 +65,10 @@ pub(crate) trait DB { async fn search_projects(&self, input: &SearchProjectsInput) -> Result<(Count, JsonString)>; /// Get some general stats. - async fn stats(&self, foundation: Option<&String>) -> Result; + async fn stats(&self, foundation: Option<&str>) -> Result; + + /// Update the number of views of the projects provided. + async fn update_projects_views(&self, data: Vec<(ProjectId, Date, Total)>) -> Result<()>; } /// DB implementation backed by PostgreSQL. @@ -195,7 +204,7 @@ impl DB for PgDB { Ok((count, projects)) } - async fn stats(&self, foundation: Option<&String>) -> Result { + async fn stats(&self, foundation: Option<&str>) -> Result { let db = self.pool.get().await?; let stats = db .query_one("select get_stats($1::text)::text", &[&foundation]) @@ -203,6 +212,16 @@ impl DB for PgDB { .get(0); Ok(stats) } + + async fn update_projects_views(&self, data: Vec<(ProjectId, Date, Total)>) -> Result<()> { + let db = self.pool.get().await?; + db.execute( + "select update_projects_views($1::bigint, $2::jsonb)", + &[&LOCK_KEY_UPDATE_PROJECTS_VIEWS, &Json(&data)], + ) + .await?; + Ok(()) + } } /// Query input used when searching for projects. diff --git a/clomonitor-apiserver/src/handlers.rs b/clomonitor-apiserver/src/handlers.rs index 3db7386d..58c742b6 100644 --- a/clomonitor-apiserver/src/handlers.rs +++ b/clomonitor-apiserver/src/handlers.rs @@ -1,5 +1,8 @@ use super::filters; -use crate::db::{DynDB, SearchProjectsInput}; +use crate::{ + db::{DynDB, SearchProjectsInput}, + views::DynVT, +}; use anyhow::Error; use askama_axum::Template; use axum::{ @@ -23,6 +26,7 @@ use std::{collections::HashMap, fmt::Display, sync::Arc}; use tera::{Context, Tera}; use time::{format_description, Date}; use tracing::error; +use uuid::Uuid; /// Index HTML document cache duration. pub const INDEX_CACHE_MAX_AGE: usize = 300; @@ -361,7 +365,7 @@ pub(crate) async fn stats( ) -> impl IntoResponse { // Get stats from database let stats = db - .stats(params.get("foundation")) + .stats(params.get("foundation").map(|p| p.as_str())) .await .map_err(internal_error)?; @@ -373,6 +377,17 @@ pub(crate) async fn stats( .map_err(internal_error) } +/// Handler used to track a project view. +pub(crate) async fn track_view( + State(vt): State, + Path(project_id): Path, +) -> impl IntoResponse { + match vt.read().await.track_view(project_id).await { + Ok(_) => StatusCode::NO_CONTENT, + Err(err) => internal_error(err), + } +} + /// Helper for mapping any error into a `500 Internal Server Error` response. fn internal_error(err: E) -> StatusCode where diff --git a/clomonitor-apiserver/src/main.rs b/clomonitor-apiserver/src/main.rs index 8b914af1..c0d94fbf 100644 --- a/clomonitor-apiserver/src/main.rs +++ b/clomonitor-apiserver/src/main.rs @@ -1,4 +1,4 @@ -use crate::db::PgDB; +use crate::{db::PgDB, views::ViewsTrackerDB}; use anyhow::{Context, Result}; use clap::Parser; use config::{Config, File}; @@ -9,7 +9,7 @@ use postgres_openssl::MakeTlsConnector; use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; -use tokio::signal; +use tokio::{signal, sync::RwLock}; use tracing::{debug, info}; use tracing_subscriber::EnvFilter; @@ -18,6 +18,7 @@ mod filters; mod handlers; mod middleware; mod router; +mod views; #[derive(Debug, Parser)] #[clap(author, version, about)] @@ -59,6 +60,9 @@ async fn main() -> Result<()> { let pool = db_cfg.create_pool(Some(Runtime::Tokio1), connector)?; let db = Arc::new(PgDB::new(pool)); + // Setup views tracker + let vt = Arc::new(RwLock::new(ViewsTrackerDB::new(db.clone()))); + // Setup and launch Prometheus exporter debug!("setting up prometheus exporter"); PrometheusBuilder::new() @@ -72,7 +76,7 @@ async fn main() -> Result<()> { // Setup and launch API HTTP server debug!("setting up apiserver"); - let router = router::setup(cfg.clone(), db)?; + let router = router::setup(cfg.clone(), db, vt.clone())?; let addr: SocketAddr = cfg.get_string("apiserver.addr")?.parse()?; info!("apiserver started"); info!("listening on {}", addr); @@ -80,8 +84,11 @@ async fn main() -> Result<()> { .serve(router.into_make_service()) .with_graceful_shutdown(shutdown_signal()) .await?; - info!("apiserver stopped"); + // Ask views tracker to stop and wait for it to finish + vt.write().await.stop().await; + + info!("apiserver stopped"); Ok(()) } diff --git a/clomonitor-apiserver/src/router.rs b/clomonitor-apiserver/src/router.rs index b3e080c2..98c29815 100644 --- a/clomonitor-apiserver/src/router.rs +++ b/clomonitor-apiserver/src/router.rs @@ -1,10 +1,10 @@ -use crate::{db::DynDB, handlers::*, middleware::metrics_collector}; +use crate::{db::DynDB, handlers::*, middleware::metrics_collector, views::DynVT}; use anyhow::Result; use axum::{ extract::FromRef, http::{header::CACHE_CONTROL, HeaderValue, StatusCode}, middleware, - routing::{get, get_service}, + routing::{get, get_service, post}, Router, }; use config::Config; @@ -27,11 +27,12 @@ pub const DOCS_CACHE_MAX_AGE: usize = 300; struct RouterState { cfg: Arc, db: DynDB, + vt: DynVT, tmpl: Arc, } /// Setup API server router. -pub(crate) fn setup(cfg: Arc, db: DynDB) -> Result { +pub(crate) fn setup(cfg: Arc, db: DynDB, vt: DynVT) -> Result { // Setup error handler let error_handler = |err: std::io::Error| async move { ( @@ -54,20 +55,21 @@ pub(crate) fn setup(cfg: Arc, db: DynDB) -> Result { // Setup API routes let api_routes = Router::new() .route("/projects/search", get(search_projects)) + .route("/projects/views/:project_id", post(track_view)) .route("/projects/:foundation/:project", get(project)) .route("/projects/:foundation/:project/badge", get(badge)) .route( "/projects/:foundation/:project/report-summary", get(report_summary_svg), ) - .route( - "/projects/:foundation/:project/snapshots/:date", - get(project_snapshot), - ) .route( "/projects/:foundation/:project/:repository/report.md", get(repository_report_md), ) + .route( + "/projects/:foundation/:project/snapshots/:date", + get(project_snapshot), + ) .route("/stats", get(stats)); // Setup router @@ -107,6 +109,7 @@ pub(crate) fn setup(cfg: Arc, db: DynDB) -> Result { .with_state(RouterState { cfg: cfg.clone(), db, + vt, tmpl, }); @@ -123,7 +126,10 @@ pub(crate) fn setup(cfg: Arc, db: DynDB) -> Result { #[cfg(test)] mod tests { use super::*; - use crate::db::{MockDB, SearchProjectsInput}; + use crate::{ + db::{MockDB, SearchProjectsInput}, + views::MockViewsTracker, + }; use axum::{ body::Body, http::{ @@ -142,11 +148,14 @@ mod tests { format_description::{self, FormatItem}, Date, }; + use tokio::sync::RwLock; use tower::ServiceExt; + use uuid::Uuid; const TESTDATA_PATH: &str = "src/testdata"; const FOUNDATION: &str = "cncf"; const PROJECT: &str = "artifact-hub"; + const PROJECT_ID: &str = "00000000-0000-0000-0000-000000000001"; const DATE: &str = "2022-10-28"; const REPOSITORY: &str = "artifact-hub"; @@ -163,7 +172,7 @@ mod tests { .times(1) .returning(|_: &str, _: &str| Box::pin(future::ready(Ok(Some("a".to_string()))))); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -205,7 +214,7 @@ mod tests { .times(1) .returning(|_: &str, _: &str| Box::pin(future::ready(Ok(None)))); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -221,7 +230,7 @@ mod tests { #[tokio::test] async fn docs_files() { - let response = setup_test_router(MockDB::new()) + let response = setup_test_router(MockDB::new(), MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -245,7 +254,7 @@ mod tests { #[tokio::test] async fn index() { - let response = setup_test_router(MockDB::new()) + let response = setup_test_router(MockDB::new(), MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -274,7 +283,7 @@ mod tests { #[tokio::test] async fn index_fallback() { - let response = setup_test_router(MockDB::new()) + let response = setup_test_router(MockDB::new(), MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -303,7 +312,7 @@ mod tests { #[tokio::test] async fn index_project() { - let response = setup_test_router(MockDB::new()) + let response = setup_test_router(MockDB::new(), MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -342,7 +351,7 @@ mod tests { )))) }); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -373,7 +382,7 @@ mod tests { .times(1) .returning(|_: &str, _: &str| Box::pin(future::ready(Ok(None)))); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -391,7 +400,7 @@ mod tests { async fn project_snapshot_invalid_date_format() { let db = MockDB::new(); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -423,7 +432,7 @@ mod tests { )))) }); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -457,7 +466,7 @@ mod tests { .times(1) .returning(|_, _, _| Box::pin(future::ready(Ok(None)))); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -481,7 +490,7 @@ mod tests { .times(1) .returning(|_: &str, _: &str| Box::pin(future::ready(Ok(None)))); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -513,7 +522,7 @@ mod tests { Box::pin(future::ready(Ok(Some(score)))) }); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -546,7 +555,7 @@ mod tests { .times(1) .returning(|_: &str, _: &str| Box::pin(future::ready(Ok(None)))); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -569,7 +578,7 @@ mod tests { .times(1) .returning(|| Box::pin(future::ready(Ok("CSV data".to_string())))); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -666,7 +675,7 @@ mod tests { Box::pin(future::ready(Ok(Some(report_md)))) }); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -699,7 +708,7 @@ mod tests { .times(1) .returning(|_: &str, _: &str, _: &str| Box::pin(future::ready(Ok(None)))); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -741,7 +750,7 @@ mod tests { )))) }); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -786,7 +795,7 @@ mod tests { #[tokio::test] async fn static_files() { - let response = setup_test_router(MockDB::new()) + let response = setup_test_router(MockDB::new(), MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -812,11 +821,11 @@ mod tests { async fn stats() { let mut db = MockDB::new(); db.expect_stats() - .withf(|v| v.as_deref() == Some(&FOUNDATION.to_string())) + .withf(|v| v.as_deref() == Some(FOUNDATION)) .times(1) .returning(|_| Box::pin(future::ready(Ok(r#"{"some": "stats"}"#.to_string())))); - let response = setup_test_router(db) + let response = setup_test_router(db, MockViewsTracker::new()) .oneshot( Request::builder() .method("GET") @@ -836,9 +845,31 @@ mod tests { ); } - fn setup_test_router(db: MockDB) -> Router { + #[tokio::test] + async fn track_view() { + let mut vt = MockViewsTracker::new(); + vt.expect_track_view() + .withf(|project_id| *project_id == Uuid::parse_str(PROJECT_ID).unwrap()) + .times(1) + .returning(|_| Box::pin(future::ready(Ok(())))); + + let response = setup_test_router(MockDB::new(), vt) + .oneshot( + Request::builder() + .method("POST") + .uri(format!("/api/projects/views/{PROJECT_ID}")) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NO_CONTENT); + } + + fn setup_test_router(db: MockDB, vt: MockViewsTracker) -> Router { let cfg = setup_test_config(); - setup(Arc::new(cfg), Arc::new(db)).unwrap() + setup(Arc::new(cfg), Arc::new(db), Arc::new(RwLock::new(vt))).unwrap() } fn setup_test_config() -> Config { diff --git a/clomonitor-apiserver/src/views.rs b/clomonitor-apiserver/src/views.rs new file mode 100644 index 00000000..a55cd80f --- /dev/null +++ b/clomonitor-apiserver/src/views.rs @@ -0,0 +1,164 @@ +use crate::db::DynDB; +use anyhow::Result; +use async_trait::async_trait; +use lazy_static::lazy_static; +#[cfg(test)] +use mockall::automock; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use time::{ + format_description::{self, FormatItem}, + Date, OffsetDateTime, +}; +use tokio::{ + sync::{broadcast, mpsc, RwLock}, + task::JoinSet, + time::Instant, +}; +use tracing::error; +use uuid::Uuid; + +/// How often projects views will be written to the database. +const FLUSH_FREQUENCY: Duration = Duration::from_secs(300); + +/// Type alias to represent a ViewsTracker trait object. +pub(crate) type DynVT = Arc>; + +/// Type alias to represent a project id. +pub(crate) type ProjectId = Uuid; + +/// Type alias to represent a views counter. +pub(crate) type Total = u32; + +/// Type alias to represent a views batch. +type Batch = HashMap; + +lazy_static! { + static ref DATE_FORMAT: Vec> = + format_description::parse("[year]-[month]-[day]").expect("format to be valid"); +} + +/// Trait that defines some operations a ViewsTracker implementation must +/// support. +#[async_trait] +#[cfg_attr(test, automock)] +pub(crate) trait ViewsTracker { + /// Track a view for the project provided. + async fn track_view(&self, project_id: ProjectId) -> Result<()>; +} + +/// ViewsTracker implementation backed by a DB instance. +pub(crate) struct ViewsTrackerDB { + views_tx: mpsc::Sender, + stop_tx: Option>, + workers: JoinSet<()>, +} + +impl ViewsTrackerDB { + /// Create a new ViewsTrackerDB instance. + pub(crate) fn new(db: DynDB) -> Self { + // Setup channels + let (views_tx, views_rx) = mpsc::channel(100); + let (batches_tx, batches_rx) = mpsc::channel(5); + let (stop_tx, _) = broadcast::channel(1); + + // Setup workers + let mut workers = JoinSet::new(); + workers.spawn(aggregator(views_rx, batches_tx, stop_tx.subscribe())); + workers.spawn(flusher(db, batches_rx)); + + Self { + views_tx, + stop_tx: Some(stop_tx), + workers, + } + } + + /// Ask the workers to stop and wait for them to finish. + pub(crate) async fn stop(&mut self) { + self.stop_tx = None; + while self.workers.join_next().await.is_some() {} + } +} + +#[async_trait] +impl ViewsTracker for ViewsTrackerDB { + async fn track_view(&self, project_id: ProjectId) -> Result<()> { + self.views_tx.send(project_id).await.map_err(Into::into) + } +} + +/// Worker that aggregates the views received on the views channel, passing the +/// resulting batches to the flusher periodically. +async fn aggregator( + mut views_rx: mpsc::Receiver, + batches_tx: mpsc::Sender, + mut stop_rx: broadcast::Receiver<()>, +) { + let first_flush = Instant::now() + FLUSH_FREQUENCY; + let mut flush_interval = tokio::time::interval_at(first_flush, FLUSH_FREQUENCY); + let mut batch: Batch = HashMap::new(); + loop { + tokio::select! { + biased; + + // Send batch to flusher every FLUSH_FREQUENCY + _ = flush_interval.tick() => { + if !batch.is_empty() { + _ = batches_tx.send(batch.clone()).await; + batch.clear(); + } + } + + // Pick next view from queue and aggregate it + Some(project_id) = views_rx.recv() => { + *batch.entry(build_key(project_id)).or_default() += 1; + } + + // Exit if the aggregator has been asked to stop + _ = stop_rx.recv() => { + if !batch.is_empty() { + _ = batches_tx.send(batch).await; + } + break + } + } + } +} + +/// Worker that stores the views batches received from the aggregator into +/// the database. +async fn flusher(db: DynDB, mut batches_rx: mpsc::Receiver) { + while let Some(batch) = batches_rx.recv().await { + // Prepare batch data for database update + let mut data: Vec<(ProjectId, Date, Total)> = batch + .iter() + .map(|(key, total)| { + let (project_id, date) = parse_key(key); + (project_id, date, *total) + }) + .collect(); + data.sort(); + + // Write data to database + if let Err(err) = db.update_projects_views(data).await { + error!("error writing projects views to database: {:#?}", err); + } + } +} + +/// Build key used to track views for a given project. +fn build_key(project_id: ProjectId) -> String { + format!( + "{}##{}", + project_id, + OffsetDateTime::now_utc().format(&DATE_FORMAT).unwrap() + ) +} + +/// Parse project views key, returning the project id and the date. +fn parse_key(key: &str) -> (ProjectId, Date) { + let mut parts = key.split("##"); + let project_id = Uuid::parse_str(parts.next().unwrap()).unwrap(); + let date = Date::parse(parts.next().unwrap(), &DATE_FORMAT).unwrap(); + (project_id, date) +} diff --git a/clomonitor-core/src/linter/checks/trademark_disclaimer.rs b/clomonitor-core/src/linter/checks/trademark_disclaimer.rs index d5bb2ed4..51f11a17 100644 --- a/clomonitor-core/src/linter/checks/trademark_disclaimer.rs +++ b/clomonitor-core/src/linter/checks/trademark_disclaimer.rs @@ -19,7 +19,7 @@ pub(crate) const CHECK_SETS: [CheckSet; 1] = [CheckSet::Community]; lazy_static! { #[rustfmt::skip] pub(crate) static ref TRADEMARK_DISCLAIMER: RegexSet = RegexSet::new(vec![ - r"https://(?:w{3}\.)?linuxfoundation.org/trademark-usage", + r"https://(?:w{3}\.)?linuxfoundation.org/(?:legal/)?trademark-usage", r"The Linux Foundation.* has registered trademarks and uses trademarks", ]).expect("exprs in TRADEMARK_DISCLAIMER to be valid"); } @@ -45,7 +45,11 @@ mod tests { #[test] fn trademark_disclaimer_match() { assert!(TRADEMARK_DISCLAIMER.is_match("https://www.linuxfoundation.org/trademark-usage")); + assert!( + TRADEMARK_DISCLAIMER.is_match("https://www.linuxfoundation.org/legal/trademark-usage") + ); assert!(TRADEMARK_DISCLAIMER.is_match("https://linuxfoundation.org/trademark-usage")); + assert!(TRADEMARK_DISCLAIMER.is_match("https://linuxfoundation.org/legal/trademark-usage")); assert!(TRADEMARK_DISCLAIMER.is_match( "The Linux Foundation® (TLF) has registered trademarks and uses trademarks." )); diff --git a/clomonitor-linter/Dockerfile b/clomonitor-linter/Dockerfile index 70b87d11..c9e6838b 100644 --- a/clomonitor-linter/Dockerfile +++ b/clomonitor-linter/Dockerfile @@ -18,7 +18,7 @@ WORKDIR /tmp RUN apk --no-cache add git make bash gcc musl-dev binutils-gold RUN git clone https://github.com/ossf/scorecard WORKDIR /tmp/scorecard -RUN git checkout v4.10.0 +RUN git checkout v4.10.2 RUN make install RUN make build diff --git a/clomonitor-tracker/Dockerfile b/clomonitor-tracker/Dockerfile index eeddf7b9..c0665aa5 100644 --- a/clomonitor-tracker/Dockerfile +++ b/clomonitor-tracker/Dockerfile @@ -18,7 +18,7 @@ WORKDIR /tmp RUN apk --no-cache add git make bash gcc musl-dev binutils-gold RUN git clone https://github.com/ossf/scorecard WORKDIR /tmp/scorecard -RUN git checkout v4.10.0 +RUN git checkout v4.10.2 RUN make install RUN make build diff --git a/data/cncf.yaml b/data/cncf.yaml index c3c1d9a6..cf5a2ece 100644 --- a/data/cncf.yaml +++ b/data/cncf.yaml @@ -246,6 +246,14 @@ check_sets: - community - code + - name: hubble + url: https://github.com/cilium/hubble + check_sets: + - code-lite + - name: tetragon + url: https://github.com/cilium/tetragon + check_sets: + - code-lite - name: cloud-custodian display_name: Cloud Custodian description: Rules engine for cloud security, cost optimization, and governance, DSL in yaml for policies to query, filter, and take actions on resources @@ -2185,31 +2193,6 @@ check_sets: - community - code -- name: tetragon - display_name: Tetragon - description: eBPF-based Security Observability and Runtime Enforcement - category: provisioning - logo_url: https://landscape.cncf.io/logos/tetragon.svg - accepted_at: "2021-10-13" - maturity: incubating - repositories: - - name: tetragon - url: https://github.com/cilium/tetragon - check_sets: - - community - - code -- name: hubble - display_name: Hubble - description: Hubble - Network, Service & Security Observability for Kubernetes using eBPF - category: observability - logo_url: https://landscape.cncf.io/logos/hubble.svg - maturity: incubating - repositories: - - name: hubble - url: https://github.com/cilium/hubble - check_sets: - - community - - code - name: kubearmor display_name: Kubearmor description: Runtime protection for Kubernetes & other cloud Workloads. Kubearmor provides a observability and policy enforcement system to restrict any diff --git a/database/migrations/functions/001_load_functions.sql b/database/migrations/functions/001_load_functions.sql index af4bedf9..88358158 100644 --- a/database/migrations/functions/001_load_functions.sql +++ b/database/migrations/functions/001_load_functions.sql @@ -5,6 +5,7 @@ {{ template "projects/register_project.sql" }} {{ template "projects/search_projects.sql" }} {{ template "projects/unregister_project.sql" }} +{{ template "projects/update_projects_views.sql" }} {{ template "repositories/get_repositories_with_checks.sql" }} {{ template "repositories/get_repository_report.sql" }} {{ template "stats/average_section_score.sql" }} diff --git a/database/migrations/functions/projects/update_projects_views.sql b/database/migrations/functions/projects/update_projects_views.sql new file mode 100644 index 00000000..453cccef --- /dev/null +++ b/database/migrations/functions/projects/update_projects_views.sql @@ -0,0 +1,16 @@ +-- update_projects_views updates the views of the projects provided. +create or replace function update_projects_views(p_lock_key bigint, p_data jsonb) +returns void as $$ + -- Make sure only one batch of updates is processed at a time + select pg_advisory_xact_lock(p_lock_key); + + -- Insert or update the corresponding views counters as needed + insert into project_views (project_id, day, total) + select + (value->>0)::uuid as project_id, + (value->>1)::date as day, + (value->>2)::integer as total + from jsonb_array_elements(p_data) + on conflict (project_id, day) do + update set total = project_views.total + excluded.total; +$$ language sql; diff --git a/database/migrations/schema/003_track_views.sql b/database/migrations/schema/003_track_views.sql new file mode 100644 index 00000000..1902e2f7 --- /dev/null +++ b/database/migrations/schema/003_track_views.sql @@ -0,0 +1,10 @@ +create table if not exists project_views ( + project_id uuid references project on delete set null, + day date not null, + total integer not null, + unique (project_id, day) + ); + +---- create above / drop below ---- + +drop table if exists project_views; diff --git a/database/tests/functions/projects/update_projects_views.sql b/database/tests/functions/projects/update_projects_views.sql new file mode 100644 index 00000000..6191c352 --- /dev/null +++ b/database/tests/functions/projects/update_projects_views.sql @@ -0,0 +1,81 @@ +-- Start transaction and plan tests +begin; +select plan(3); + +-- Declare some variables +\set lockKey 1 + +-- Seed some data +insert into foundation values ('cncf', 'CNCF', 'http://127.0.0.1:8080/cncf.yaml'); +insert into project ( + project_id, + name, + category, + accepted_at, + maturity, + foundation_id +) values ( + '00000000-0000-0000-0000-000000000001', + 'project1', + 'category1', + '2022-12-19', + 'sandbox', + 'cncf' +); +insert into project ( + project_id, + name, + category, + accepted_at, + maturity, + foundation_id +) values ( + '00000000-0000-0000-0000-000000000002', + 'project2', + 'category1', + '2022-12-19', + 'incubating', + 'cncf' +); + +-- Run some tests +select update_projects_views(:lockKey, '[ + ["00000000-0000-0000-0000-000000000001", "2022-12-19", 10] +]'); +select results_eq( + 'select * from project_views', + $$ values + ('00000000-0000-0000-0000-000000000001'::uuid, '2022-12-19'::date, 10) + $$, + 'First run: one insert' +); +select update_projects_views(:lockKey, '[ + ["00000000-0000-0000-0000-000000000001", "2022-12-19", 10] +]'); +select results_eq( + 'select * from project_views', + $$ values + ('00000000-0000-0000-0000-000000000001'::uuid, '2022-12-19'::date, 20) + $$, + 'Second run: one update' +); +select update_projects_views(:lockKey, '[ + ["00000000-0000-0000-0000-000000000001", "2022-12-19", 10], + ["00000000-0000-0000-0000-000000000001", "2022-12-20", 10], + ["00000000-0000-0000-0000-000000000002", "2022-12-20", 10], + ["00000000-0000-0000-0000-000000000002", "2022-12-21", 5] +]'); +select results_eq( + 'select * from project_views', + $$ values + ('00000000-0000-0000-0000-000000000001'::uuid, '2022-12-19'::date, 30), + ('00000000-0000-0000-0000-000000000001'::uuid, '2022-12-20'::date, 10), + ('00000000-0000-0000-0000-000000000002'::uuid, '2022-12-20'::date, 10), + ('00000000-0000-0000-0000-000000000002'::uuid, '2022-12-21'::date, 5) + $$, + 'Third run: one update and three inserts' +); + +-- Finish tests and rollback transaction +select * from finish(); +rollback; diff --git a/database/tests/schema/schema.sql b/database/tests/schema/schema.sql index d46328f1..b83e1b79 100644 --- a/database/tests/schema/schema.sql +++ b/database/tests/schema/schema.sql @@ -1,6 +1,6 @@ -- Start transaction and plan tests begin; -select plan(27); +select plan(31); -- Check expected extension exist select has_extension('pgcrypto'); @@ -9,6 +9,7 @@ select has_extension('pgcrypto'); select has_table('foundation'); select has_table('project'); select has_table('project_snapshot'); +select has_table('project_views'); select has_table('report'); select has_table('repository'); @@ -42,6 +43,11 @@ select columns_are('project_snapshot', array[ 'date', 'data' ]); +select columns_are('project_views', array[ + 'project_id', + 'day', + 'total' +]); select columns_are('report', array[ 'report_id', 'check_sets', @@ -75,6 +81,9 @@ select indexes_are('project', array[ select indexes_are('project_snapshot', array[ 'project_snapshot_pkey' ]); +select indexes_are('project_views', array[ + 'project_views_project_id_day_key' +]); select indexes_are('report', array[ 'report_pkey', 'report_repository_id_idx', @@ -95,6 +104,7 @@ select has_function('get_project_passed_checks'); select has_function('register_project'); select has_function('search_projects'); select has_function('unregister_project'); +select has_function('update_projects_views'); -- Repositories select has_function('get_repositories_with_checks'); select has_function('get_repository_report'); diff --git a/docs/checks.md b/docs/checks.md index 6acd80cd..6b1cd34a 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -624,6 +624,6 @@ This check passes if: - The Linux Foundation trademark disclaimer is found in the content of the website configured in Github. Regexps used: ```sh -"https://(?:w{3}\.)?linuxfoundation.org/trademark-usage" +"https://(?:w{3}\.)?linuxfoundation.org/(?:legal/)?trademark-usage" "The Linux Foundation.* has registered trademarks and uses trademarks" ``` diff --git a/web/package.json b/web/package.json index 48be162d..052e099a 100644 --- a/web/package.json +++ b/web/package.json @@ -16,9 +16,9 @@ "react-dom": "^17.0.2", "react-icons": "^4.7.1", "react-markdown": "^8.0.3", - "react-router-dom": "^6.4.5", + "react-router-dom": "^6.6.0", "react-syntax-highlighter": "^15.5.0", - "tinycolor2": "^1.4.2", + "tinycolor2": "^1.5.0", "ua-parser-js": "^1.0.32" }, "devDependencies": { @@ -27,7 +27,7 @@ "@testing-library/user-event": "^14.4.3", "@types/jest": "^29.2.4", "@types/lodash": "^4.14.191", - "@types/node": "^18.11.15", + "@types/node": "^18.11.17", "@types/react": "^18.0.26", "@types/react-date-range": "^1.4.3", "@types/react-dom": "^18.0.3", @@ -40,12 +40,12 @@ "postcss": "^8.2.20", "prettier": "^2.8.1", "react-scripts": "5.0.1", - "sass": "^1.56.2", + "sass": "^1.57.1", "shx": "^0.3.4", "typescript": "^4.9.4" }, "resolutions": { - "react-scripts/**/minimatch": "^5.1.0", + "react-scripts/**/minimatch": "^5.1.2", "react-scripts/**/nth-check": "^2.0.1", "react-scripts/**/postcss": "^8.2.13" }, diff --git a/web/src/api/index.test.tsx b/web/src/api/index.test.tsx index fd318b85..e0a462d9 100644 --- a/web/src/api/index.test.tsx +++ b/web/src/api/index.test.tsx @@ -115,5 +115,110 @@ cncf,akri,https://github.com/project-akri/akri,"{community,code}",t,t,t,t,t,t,t, expect(response).toEqual(csv); }); }); + + describe('getRepositoryReportMD', () => { + it('success', async () => { + const reportSample = ` + ## CLOMonitor report + + ### Summary + + **Repository**: artifact-hub + **URL**: https://github.com/artifacthub/hub + **Checks sets**: \`COMMUNITY\` + \`CODE\` + **Score**: 95 + + ### Checks passed per category + + | Category | Score | + | :----------------- | --------: | + | Documentation | 87% | + | License | 100% | + | Best Practices | 95% | + | Security | 100% | + | Legal | 100% | + + ## Checks + + ### Documentation [87%] + + - [x] [Adopters](https://github.com/artifacthub/hub/blob/master/ADOPTERS.md) ([_docs_](https://clomonitor.io/docs/topics/checks/#adopters)) + - [x] Changelog ([_docs_](https://clomonitor.io/docs/topics/checks/#changelog)) + - [x] [Code of conduct](https://github.com/artifacthub/hub/blob/master/code-of-conduct.md) ([_docs_](https://clomonitor.io/docs/topics/checks/#code-of-conduct)) + - [x] [Contributing](https://github.com/artifacthub/hub/blob/master/CONTRIBUTING.md) ([_docs_](https://clomonitor.io/docs/topics/checks/#contributing)) + - [ ] Governance ([_docs_](https://clomonitor.io/docs/topics/checks/#governance)) + - [x] [Maintainers](https://github.com/artifacthub/hub/blob/master/OWNERS) ([_docs_](https://clomonitor.io/docs/topics/checks/#maintainers)) + - [x] [Readme](https://github.com/artifacthub/hub/blob/master/README.md) ([_docs_](https://clomonitor.io/docs/topics/checks/#readme)) + - [ ] Roadmap ([_docs_](https://clomonitor.io/docs/topics/checks/#roadmap)) + - [x] [Website](https://artifacthub.io) ([_docs_](https://clomonitor.io/docs/topics/checks/#website)) + + ### License [100%] + + - [x] Apache-2.0 ([_docs_](https://clomonitor.io/docs/topics/checks/#spdx-id)) + - [x] Approved license ([_docs_](https://clomonitor.io/docs/topics/checks/#approved-license)) + - [x] [License scanning](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fartifacthub%2Fhub?ref=badge_shield) ([_docs_](https://clomonitor.io/docs/topics/checks/#license-scanning)) + + ### Best Practices [95%] + + - [ ] Analytics ([_docs_](https://clomonitor.io/docs/topics/checks/#analytics)) + - [x] [Artifact Hub badge](https://artifacthub.io/packages/helm/artifact-hub/artifact-hub) ([_docs_](https://clomonitor.io/docs/topics/checks/#artifact-hub-badge)) + - [x] Contributor License Agreement ([_docs_](https://clomonitor.io/docs/topics/checks/#contributor-license-agreement)) \`EXEMPT\` + - [x] Community meeting ([_docs_](https://clomonitor.io/docs/topics/checks/#community-meeting)) + - [x] Developer Certificate of Origin ([_docs_](https://clomonitor.io/docs/topics/checks/#developer-certificate-of-origin)) + - [x] [Github discussions](https://github.com/artifacthub/hub/discussions/2621) ([_docs_](https://clomonitor.io/docs/topics/checks/#github-discussions)) + - [x] [OpenSSF badge](https://bestpractices.coreinfrastructure.org/projects/4106) ([_docs_](https://clomonitor.io/docs/topics/checks/#openssf-badge)) + - [x] [Recent release](https://github.com/artifacthub/hub/releases/tag/v1.11.0) ([_docs_](https://clomonitor.io/docs/topics/checks/#recent-release)) + - [x] Slack precense ([_docs_](https://clomonitor.io/docs/topics/checks/#slack-presence)) + + ### Security [100%] + + - [x] Binary artifacts ([_docs_](https://clomonitor.io/docs/topics/checks/#binary-artifacts-from-openssf-scorecard)) + - [x] Code review ([_docs_](https://clomonitor.io/docs/topics/checks/#code-review-from-openssf-scorecard)) + - [x] Dangerous workflow ([_docs_](https://clomonitor.io/docs/topics/checks/#dangerous-workflow-from-openssf-scorecard)) + - [x] Dependency update tool ([_docs_](https://clomonitor.io/docs/topics/checks/#dependency-update-tool-from-openssf-scorecard)) + - [x] Maintained ([_docs_](https://clomonitor.io/docs/topics/checks/#maintained-from-openssf-scorecard)) + - [x] Software bill of materials (SBOM) ([_docs_](https://clomonitor.io/docs/topics/checks/#software-bill-of-materials-sbom)) + - [x] [Security policy](https://github.com/artifacthub/hub/blob/master/SECURITY.md) ([_docs_](https://clomonitor.io/docs/topics/checks/#security-policy)) + - [x] Signed releases ([_docs_](https://clomonitor.io/docs/topics/checks/#signed-releases-from-openssf-scorecard)) + - [x] Token permissions ([_docs_](https://clomonitor.io/docs/topics/checks/#token-permissions-from-openssf-scorecard)) + + ### Legal [100%] + + - [x] Trademark disclaimer ([_docs_](https://clomonitor.io/docs/topics/checks/#trademark-disclaimer)) \`EXEMPT\` + + For more information about the checks sets available and how each of the checks work, please see the [CLOMonitor's documentation](https://clomonitor.io/docs/topics/checks/). + `; + fetchMock.mockResponse(reportSample, { + headers: { + 'content-type': 'text/plain; charset=utf-8', + }, + status: 200, + }); + + const response = await API.getRepositoryReportMD('foundation', 'proj', 'repo'); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0][0]).toEqual('/api/projects/foundation/proj/repo/report.md'); + expect(response).toEqual(reportSample); + }); + }); + + describe('trackView', () => { + it('success', async () => { + fetchMock.mockResponse('', { + headers: { + 'content-type': 'text/plain; charset=utf-8', + }, + status: 204, + }); + + const response = await API.trackView('projectID'); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0][0]).toEqual('/api/projects/views/projectID'); + expect(fetchMock.mock.calls[0][1]!.method).toBe('POST'); + expect(response).toBe(''); + }); + }); }); }); diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 3efca5d4..f70fa4eb 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -163,6 +163,15 @@ class API_CLASS { url: `${this.API_BASE_URL}/projects/${foundation}/${project}/${repoName}/report.md`, }); } + + public trackView(projectId: string): Promise { + return this.apiFetch({ + url: `${this.API_BASE_URL}/projects/views/${projectId}`, + opts: { + method: 'POST', + }, + }); + } } const API = new API_CLASS(); diff --git a/web/src/layout/detail/index.tsx b/web/src/layout/detail/index.tsx index 692a02f5..67a73c2c 100644 --- a/web/src/layout/detail/index.tsx +++ b/web/src/layout/detail/index.tsx @@ -69,12 +69,21 @@ const Detail = (props: Props) => { [location.hash] ); + async function trackView(projectID: string) { + try { + API.trackView(projectID); + } catch { + // Do not do anything + } + } + async function fetchProjectDetail() { scrollToTop(); // Go to top when a new project is fetched setIsLoadingProject(true); props.setInvisibleFooter(true); try { const projectDetail = await API.getProjectDetail(project!, foundation!); + trackView(projectDetail.id); setDetail(projectDetail); setSnapshots(projectDetail.snapshots || []); updateMetaIndex(projectDetail.display_name || projectDetail.name, projectDetail.description); diff --git a/web/src/layout/detail/repositories/RepositoryReportModal.test.tsx b/web/src/layout/detail/repositories/RepositoryReportModal.test.tsx new file mode 100644 index 00000000..abaeeac4 --- /dev/null +++ b/web/src/layout/detail/repositories/RepositoryReportModal.test.tsx @@ -0,0 +1,178 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { mocked } from 'jest-mock'; +import ReactRouter, { BrowserRouter as Router } from 'react-router-dom'; + +import API from '../../../api'; +import { AppContext } from '../../../context/AppContextProvider'; +import { SortBy, SortDirection } from '../../../types'; +import RepositoryReportModal from './RepositoryReportModal'; + +jest.mock('../../../api'); +jest.mock('react-router-dom', () => ({ + ...(jest.requireActual('react-router-dom') as any), + useParams: jest.fn(), + useLocation: jest.fn(), +})); + +const mockCtx = { + prefs: { + search: { limit: 20, sort: { by: SortBy.Name, direction: SortDirection.ASC } }, + theme: { effective: 'light', configured: 'light' }, + }, +}; + +const reportSample = ` +## CLOMonitor report + +### Summary + +**Repository**: artifact-hub +**URL**: https://github.com/artifacthub/hub +**Checks sets**: \`COMMUNITY\` + \`CODE\` +**Score**: 95 + +### Checks passed per category + +| Category | Score | +| :----------------- | --------: | +| Documentation | 87% | +| License | 100% | +| Best Practices | 95% | +| Security | 100% | +| Legal | 100% | + +## Checks + +### Documentation [87%] + + - [x] [Adopters](https://github.com/artifacthub/hub/blob/master/ADOPTERS.md) ([_docs_](https://clomonitor.io/docs/topics/checks/#adopters)) + - [x] Changelog ([_docs_](https://clomonitor.io/docs/topics/checks/#changelog)) + - [x] [Code of conduct](https://github.com/artifacthub/hub/blob/master/code-of-conduct.md) ([_docs_](https://clomonitor.io/docs/topics/checks/#code-of-conduct)) + - [x] [Contributing](https://github.com/artifacthub/hub/blob/master/CONTRIBUTING.md) ([_docs_](https://clomonitor.io/docs/topics/checks/#contributing)) + - [ ] Governance ([_docs_](https://clomonitor.io/docs/topics/checks/#governance)) + - [x] [Maintainers](https://github.com/artifacthub/hub/blob/master/OWNERS) ([_docs_](https://clomonitor.io/docs/topics/checks/#maintainers)) + - [x] [Readme](https://github.com/artifacthub/hub/blob/master/README.md) ([_docs_](https://clomonitor.io/docs/topics/checks/#readme)) + - [ ] Roadmap ([_docs_](https://clomonitor.io/docs/topics/checks/#roadmap)) + - [x] [Website](https://artifacthub.io) ([_docs_](https://clomonitor.io/docs/topics/checks/#website)) + +### License [100%] + + - [x] Apache-2.0 ([_docs_](https://clomonitor.io/docs/topics/checks/#spdx-id)) + - [x] Approved license ([_docs_](https://clomonitor.io/docs/topics/checks/#approved-license)) + - [x] [License scanning](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fartifacthub%2Fhub?ref=badge_shield) ([_docs_](https://clomonitor.io/docs/topics/checks/#license-scanning)) + +### Best Practices [95%] + + - [ ] Analytics ([_docs_](https://clomonitor.io/docs/topics/checks/#analytics)) + - [x] [Artifact Hub badge](https://artifacthub.io/packages/helm/artifact-hub/artifact-hub) ([_docs_](https://clomonitor.io/docs/topics/checks/#artifact-hub-badge)) + - [x] Contributor License Agreement ([_docs_](https://clomonitor.io/docs/topics/checks/#contributor-license-agreement)) \`EXEMPT\` + - [x] Community meeting ([_docs_](https://clomonitor.io/docs/topics/checks/#community-meeting)) + - [x] Developer Certificate of Origin ([_docs_](https://clomonitor.io/docs/topics/checks/#developer-certificate-of-origin)) + - [x] [Github discussions](https://github.com/artifacthub/hub/discussions/2621) ([_docs_](https://clomonitor.io/docs/topics/checks/#github-discussions)) + - [x] [OpenSSF badge](https://bestpractices.coreinfrastructure.org/projects/4106) ([_docs_](https://clomonitor.io/docs/topics/checks/#openssf-badge)) + - [x] [Recent release](https://github.com/artifacthub/hub/releases/tag/v1.11.0) ([_docs_](https://clomonitor.io/docs/topics/checks/#recent-release)) + - [x] Slack precense ([_docs_](https://clomonitor.io/docs/topics/checks/#slack-presence)) + +### Security [100%] + + - [x] Binary artifacts ([_docs_](https://clomonitor.io/docs/topics/checks/#binary-artifacts-from-openssf-scorecard)) + - [x] Code review ([_docs_](https://clomonitor.io/docs/topics/checks/#code-review-from-openssf-scorecard)) + - [x] Dangerous workflow ([_docs_](https://clomonitor.io/docs/topics/checks/#dangerous-workflow-from-openssf-scorecard)) + - [x] Dependency update tool ([_docs_](https://clomonitor.io/docs/topics/checks/#dependency-update-tool-from-openssf-scorecard)) + - [x] Maintained ([_docs_](https://clomonitor.io/docs/topics/checks/#maintained-from-openssf-scorecard)) + - [x] Software bill of materials (SBOM) ([_docs_](https://clomonitor.io/docs/topics/checks/#software-bill-of-materials-sbom)) + - [x] [Security policy](https://github.com/artifacthub/hub/blob/master/SECURITY.md) ([_docs_](https://clomonitor.io/docs/topics/checks/#security-policy)) + - [x] Signed releases ([_docs_](https://clomonitor.io/docs/topics/checks/#signed-releases-from-openssf-scorecard)) + - [x] Token permissions ([_docs_](https://clomonitor.io/docs/topics/checks/#token-permissions-from-openssf-scorecard)) + +### Legal [100%] + + - [x] Trademark disclaimer ([_docs_](https://clomonitor.io/docs/topics/checks/#trademark-disclaimer)) \`EXEMPT\` + +For more information about the checks sets available and how each of the checks work, please see the [CLOMonitor's documentation](https://clomonitor.io/docs/topics/checks/). +`; + +const mockOnCloseModal = jest.fn(); + +const defaultProps = { + repoName: 'repo', + openStatus: true, + onCloseModal: mockOnCloseModal, +}; + +describe('RepositoryReportModal', () => { + beforeEach(() => { + jest.spyOn(ReactRouter, 'useParams').mockReturnValue({ project: 'proj', foundation: 'cncf' }); + jest.spyOn(ReactRouter, 'useLocation').mockReturnValue({ + pathname: '/projects/cncf/artifact-hub/artifact-hub', + search: '', + hash: '', + state: { currentSearch: '?maturity=sandbox&rating=a&page=1' }, + key: 'key', + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('creates snapshot', async () => { + mocked(API).getRepositoryReportMD.mockResolvedValue(reportSample); + const { asFragment } = render( + + + + + + ); + + await waitFor(() => { + expect(API.getRepositoryReportMD).toHaveBeenCalledTimes(1); + }); + + expect(await screen.findByText('Repository report')).toBeInTheDocument(); + + expect(asFragment()).toMatchSnapshot(); + }); + + describe('Render', () => { + it('renders proper content', async () => { + mocked(API).getRepositoryReportMD.mockResolvedValue(reportSample); + render( + + + + + + ); + + await waitFor(() => { + expect(API.getRepositoryReportMD).toHaveBeenCalledTimes(1); + expect(API.getRepositoryReportMD).toHaveBeenCalledWith('cncf', 'proj', 'repo'); + }); + + expect(await screen.findByText('Repository report')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Copy to clipboard' })).toBeInTheDocument(); + }); + + it('displays loading while getting markdown', async () => { + mocked(API).getRepositoryReportMD.mockResolvedValue(reportSample); + + render( + + + + + + ); + + expect(screen.getByRole('status')).toBeInTheDocument(); + + await waitFor(() => { + expect(API.getRepositoryReportMD).toHaveBeenCalledTimes(1); + }); + + expect(await screen.findByText('Repository report')).toBeInTheDocument(); + }); + }); +}); diff --git a/web/src/layout/detail/repositories/__snapshots__/RepositoryReportModal.test.tsx.snap b/web/src/layout/detail/repositories/__snapshots__/RepositoryReportModal.test.tsx.snap new file mode 100644 index 00000000..a9869089 --- /dev/null +++ b/web/src/layout/detail/repositories/__snapshots__/RepositoryReportModal.test.tsx.snap @@ -0,0 +1,1608 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RepositoryReportModal creates snapshot 1`] = ` + +
+ + +`; diff --git a/web/yarn.lock b/web/yarn.lock index 09517b63..936f66de 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1190,25 +1190,25 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36" integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg== -"@eslint/eslintrc@^1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95" - integrity sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg== +"@eslint/eslintrc@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.0.tgz#8ec64e0df3e7a1971ee1ff5158da87389f167a63" + integrity sha512-7yfvXy6MWLgWSFsLhz5yH3iQ52St8cdUY6FoGieKkRDVxuxmrNuUetIuu6cmjNWwniUHiWXjxCr5tTXDrbYS5A== dependencies: ajv "^6.12.4" debug "^4.3.2" espree "^9.4.0" - globals "^13.15.0" + globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@humanwhocodes/config-array@^0.11.6": - version "0.11.7" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.7.tgz#38aec044c6c828f6ed51d5d7ae3d9b9faf6dbb0f" - integrity sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw== +"@humanwhocodes/config-array@^0.11.8": + version "0.11.8" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" + integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== dependencies: "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" @@ -1572,10 +1572,10 @@ schema-utils "^3.0.0" source-map "^0.7.3" -"@remix-run/router@1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.0.5.tgz#d5c65626add4c3c185a89aa5bd38b1e42daec075" - integrity sha512-my0Mycd+jruq/1lQuO5LBB6WTlL/e8DTCYWp44DfMTDcXz8DcTlgF0ISaLsGewt+ctHN+yA8xMq3q/N7uWJPug== +"@remix-run/router@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.2.0.tgz#54eff8306938b64c521f4a9ed313d33a91ef019a" + integrity sha512-GO82KYYTWPRCgdNtnheaZG3LcViUlxRFlHM7ykh7N+ufoXi6PVIHoP+9RUG/vuzl2hr9i/h6EA1Eq+2HpqJ0gQ== "@rollup/plugin-babel@^5.2.0": version "5.3.1" @@ -1910,7 +1910,7 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.31": version "4.17.31" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz#a1139efeab4e7323834bb0226e62ac019f474b2f" integrity sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q== @@ -1920,12 +1920,12 @@ "@types/range-parser" "*" "@types/express@*", "@types/express@^4.17.13": - version "4.17.14" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.14.tgz#143ea0557249bc1b3b54f15db4c81c3d4eb3569c" - integrity sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg== + version "4.17.15" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.15.tgz#9290e983ec8b054b65a5abccb610411953d417ff" + integrity sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ== dependencies: "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.18" + "@types/express-serve-static-core" "^4.17.31" "@types/qs" "*" "@types/serve-static" "*" @@ -2019,10 +2019,10 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== -"@types/node@*", "@types/node@^18.11.15": - version "18.11.15" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.15.tgz#de0e1fbd2b22b962d45971431e2ae696643d3f5d" - integrity sha512-VkhBbVo2+2oozlkdHXLrb3zjsRkpdnaU2bXmX8Wgle3PUi569eLRaHGlgETQHR7lLL1w7GiG3h9SnePhxNDecw== +"@types/node@*", "@types/node@^18.11.17": + version "18.11.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.17.tgz#5c009e1d9c38f4a2a9d45c0b0c493fe6cdb4bcb5" + integrity sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng== "@types/parse-json@^4.0.0": version "4.0.0" @@ -2030,9 +2030,9 @@ integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/prettier@^2.1.5": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e" - integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow== + version "2.7.2" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0" + integrity sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg== "@types/prop-types@*", "@types/prop-types@^15.0.0": version "15.7.5" @@ -2211,13 +2211,13 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^5.5.0": - version "5.46.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.0.tgz#9a96a713b9616c783501a3c1774c9e2b40217ad0" - integrity sha512-QrZqaIOzJAjv0sfjY4EjbXUi3ZOFpKfzntx22gPGr9pmFcTjcFw/1sS1LJhEubfAGwuLjNrPV0rH+D1/XZFy7Q== + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.47.0.tgz#dadb79df3b0499699b155839fd6792f16897d910" + integrity sha512-AHZtlXAMGkDmyLuLZsRpH3p4G/1iARIwc/T0vIem2YB+xW6pZaXYXzCBnZSF/5fdM97R9QqZWZ+h3iW10XgevQ== dependencies: - "@typescript-eslint/scope-manager" "5.46.0" - "@typescript-eslint/type-utils" "5.46.0" - "@typescript-eslint/utils" "5.46.0" + "@typescript-eslint/scope-manager" "5.47.0" + "@typescript-eslint/type-utils" "5.47.0" + "@typescript-eslint/utils" "5.47.0" debug "^4.3.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" @@ -2226,78 +2226,78 @@ tsutils "^3.21.0" "@typescript-eslint/experimental-utils@^5.0.0": - version "5.46.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.46.0.tgz#d691382f8cff25b646e68a2bf55ca30e3dd343d3" - integrity sha512-iMnpijlNNLL+OPIzLadOYQzHsPQ2FW6Qcd5+4DpUv9lQN4Kl+AGxjv0dx+dXPgJfDpj9Q8ePlbROdKLjQydHqg== + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.47.0.tgz#60f26e62d948f9977488825730007ec350bc1e44" + integrity sha512-DAP8xOaTAJLxouU0QrATiw8o/OHxxbUBXtkf9v+bCCU6tbJUn24xwB1dHFw3b5wYq4XvC1z5lYEN0g/Rx1sjzA== dependencies: - "@typescript-eslint/utils" "5.46.0" + "@typescript-eslint/utils" "5.47.0" "@typescript-eslint/parser@^5.5.0": - version "5.46.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.46.0.tgz#002d8e67122947922a62547acfed3347cbf2c0b6" - integrity sha512-joNO6zMGUZg+C73vwrKXCd8usnsmOYmgW/w5ZW0pG0RGvqeznjtGDk61EqqTpNrFLUYBW2RSBFrxdAZMqA4OZA== + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.47.0.tgz#62e83de93499bf4b500528f74bf2e0554e3a6c8d" + integrity sha512-udPU4ckK+R1JWCGdQC4Qa27NtBg7w020ffHqGyAK8pAgOVuNw7YaKXGChk+udh+iiGIJf6/E/0xhVXyPAbsczw== dependencies: - "@typescript-eslint/scope-manager" "5.46.0" - "@typescript-eslint/types" "5.46.0" - "@typescript-eslint/typescript-estree" "5.46.0" + "@typescript-eslint/scope-manager" "5.47.0" + "@typescript-eslint/types" "5.47.0" + "@typescript-eslint/typescript-estree" "5.47.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.46.0": - version "5.46.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.46.0.tgz#60790b14d0c687dd633b22b8121374764f76ce0d" - integrity sha512-7wWBq9d/GbPiIM6SqPK9tfynNxVbfpihoY5cSFMer19OYUA3l4powA2uv0AV2eAZV6KoAh6lkzxv4PoxOLh1oA== +"@typescript-eslint/scope-manager@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.47.0.tgz#f58144a6b0ff58b996f92172c488813aee9b09df" + integrity sha512-dvJab4bFf7JVvjPuh3sfBUWsiD73aiftKBpWSfi3sUkysDQ4W8x+ZcFpNp7Kgv0weldhpmMOZBjx1wKN8uWvAw== dependencies: - "@typescript-eslint/types" "5.46.0" - "@typescript-eslint/visitor-keys" "5.46.0" + "@typescript-eslint/types" "5.47.0" + "@typescript-eslint/visitor-keys" "5.47.0" -"@typescript-eslint/type-utils@5.46.0": - version "5.46.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.46.0.tgz#3a4507b3b437e2fd9e95c3e5eea5ae16f79d64b3" - integrity sha512-dwv4nimVIAsVS2dTA0MekkWaRnoYNXY26dKz8AN5W3cBFYwYGFQEqm/cG+TOoooKlncJS4RTbFKgcFY/pOiBCg== +"@typescript-eslint/type-utils@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.47.0.tgz#2b440979c574e317d3473225ae781f292c99e55d" + integrity sha512-1J+DFFrYoDUXQE1b7QjrNGARZE6uVhBqIvdaXTe5IN+NmEyD68qXR1qX1g2u4voA+nCaelQyG8w30SAOihhEYg== dependencies: - "@typescript-eslint/typescript-estree" "5.46.0" - "@typescript-eslint/utils" "5.46.0" + "@typescript-eslint/typescript-estree" "5.47.0" + "@typescript-eslint/utils" "5.47.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.46.0": - version "5.46.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.46.0.tgz#f4d76622a996b88153bbd829ea9ccb9f7a5d28bc" - integrity sha512-wHWgQHFB+qh6bu0IAPAJCdeCdI0wwzZnnWThlmHNY01XJ9Z97oKqKOzWYpR2I83QmshhQJl6LDM9TqMiMwJBTw== +"@typescript-eslint/types@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.47.0.tgz#67490def406eaa023dbbd8da42ee0d0c9b5229d3" + integrity sha512-eslFG0Qy8wpGzDdYKu58CEr3WLkjwC5Usa6XbuV89ce/yN5RITLe1O8e+WFEuxnfftHiJImkkOBADj58ahRxSg== -"@typescript-eslint/typescript-estree@5.46.0": - version "5.46.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.0.tgz#a6c2b84b9351f78209a1d1f2d99ca553f7fa29a5" - integrity sha512-kDLNn/tQP+Yp8Ro2dUpyyVV0Ksn2rmpPpB0/3MO874RNmXtypMwSeazjEN/Q6CTp8D7ExXAAekPEcCEB/vtJkw== +"@typescript-eslint/typescript-estree@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.47.0.tgz#ed971a11c5c928646d6ba7fc9dfdd6e997649aca" + integrity sha512-LxfKCG4bsRGq60Sqqu+34QT5qT2TEAHvSCCJ321uBWywgE2dS0LKcu5u+3sMGo+Vy9UmLOhdTw5JHzePV/1y4Q== dependencies: - "@typescript-eslint/types" "5.46.0" - "@typescript-eslint/visitor-keys" "5.46.0" + "@typescript-eslint/types" "5.47.0" + "@typescript-eslint/visitor-keys" "5.47.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.46.0", "@typescript-eslint/utils@^5.13.0": - version "5.46.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.46.0.tgz#600cd873ba471b7d8b0b9f35de34cf852c6fcb31" - integrity sha512-4O+Ps1CRDw+D+R40JYh5GlKLQERXRKW5yIQoNDpmXPJ+C7kaPF9R7GWl+PxGgXjB3PQCqsaaZUpZ9dG4U6DO7g== +"@typescript-eslint/utils@5.47.0", "@typescript-eslint/utils@^5.13.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.47.0.tgz#b5005f7d2696769a1fdc1e00897005a25b3a0ec7" + integrity sha512-U9xcc0N7xINrCdGVPwABjbAKqx4GK67xuMV87toI+HUqgXj26m6RBp9UshEXcTrgCkdGYFzgKLt8kxu49RilDw== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.46.0" - "@typescript-eslint/types" "5.46.0" - "@typescript-eslint/typescript-estree" "5.46.0" + "@typescript-eslint/scope-manager" "5.47.0" + "@typescript-eslint/types" "5.47.0" + "@typescript-eslint/typescript-estree" "5.47.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.46.0": - version "5.46.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.0.tgz#36d87248ae20c61ef72404bcd61f14aa2563915f" - integrity sha512-E13gBoIXmaNhwjipuvQg1ByqSAu/GbEpP/qzFihugJ+MomtoJtFAJG/+2DRPByf57B863m0/q7Zt16V9ohhANw== +"@typescript-eslint/visitor-keys@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.47.0.tgz#4aca4efbdf6209c154df1f7599852d571b80bb45" + integrity sha512-ByPi5iMa6QqDXe/GmT/hR6MZtVPi0SqMQPDx15FczCBXJo/7M8T88xReOALAfpBLm+zxpPfmhuEvPb577JRAEg== dependencies: - "@typescript-eslint/types" "5.46.0" + "@typescript-eslint/types" "5.47.0" eslint-visitor-keys "^3.3.0" "@webassemblyjs/ast@1.11.1": @@ -2487,9 +2487,9 @@ acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0: integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== address@^1.0.1, address@^1.1.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/address/-/address-1.2.1.tgz#25bb61095b7522d65b357baa11bc05492d4c8acd" - integrity sha512-B+6bi5D34+fDYENiH5qOlA0cV2rAGKuWZ9LeyUUehbXy8e0VS9e498yO0Jeeh+iM+6KbfudHTFjXw2MmJD4QRA== + version "1.2.2" + resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" + integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== adjust-sourcemap-loader@^4.0.0: version "4.0.0" @@ -2749,9 +2749,9 @@ available-typed-arrays@^1.0.5: integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== axe-core@^4.4.3: - version "4.5.2" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.5.2.tgz#823fdf491ff717ac3c58a52631d4206930c1d9f7" - integrity sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA== + version "4.6.1" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.1.tgz#79cccdee3e3ab61a8f42c458d4123a6768e6fbce" + integrity sha512-lCZN5XRuOnpG4bpMq8v0khrWtUOn+i8lZSb6wHZH56ZfbIEv6XwJV84AAueh9/zi7qPVJ/E4yz6fmsiyOmXR4w== axobject-query@^2.2.0: version "2.2.0" @@ -3075,9 +3075,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426: - version "1.0.30001439" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz#ab7371faeb4adff4b74dad1718a6fd122e45d9cb" - integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A== + version "1.0.30001441" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz#987437b266260b640a23cd18fbddb509d7f69f3e" + integrity sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg== case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" @@ -3437,12 +3437,12 @@ css-has-pseudo@^3.0.4: postcss-selector-parser "^6.0.9" css-loader@^6.5.1: - version "6.7.2" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.2.tgz#26bc22401b5921686a10fbeba75d124228302304" - integrity sha512-oqGbbVcBJkm8QwmnNzrFrWTnudnRZC+1eXikLJl0n4ljcfotgRifpg2a1lKy8jTrc4/d9A/ap1GFq1jDKG7J+Q== + version "6.7.3" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.3.tgz#1e8799f3ccc5874fdd55461af51137fcc5befbcd" + integrity sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ== dependencies: icss-utils "^5.1.0" - postcss "^8.4.18" + postcss "^8.4.19" postcss-modules-extract-imports "^3.0.0" postcss-modules-local-by-default "^4.0.0" postcss-modules-scope "^3.0.0" @@ -4293,12 +4293,12 @@ eslint-webpack-plugin@^3.1.1: schema-utils "^4.0.0" eslint@^8.3.0: - version "8.29.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.29.0.tgz#d74a88a20fb44d59c51851625bc4ee8d0ec43f87" - integrity sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg== + version "8.30.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.30.0.tgz#83a506125d089eef7c5b5910eeea824273a33f50" + integrity sha512-MGADB39QqYuzEGov+F/qb18r4i7DohCDOfatHaxI2iGlPuC65bwG2gxgO+7DkyL38dRFaRH7RaRAgU6JKL9rMQ== dependencies: - "@eslint/eslintrc" "^1.3.3" - "@humanwhocodes/config-array" "^0.11.6" + "@eslint/eslintrc" "^1.4.0" + "@humanwhocodes/config-array" "^0.11.8" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" ajv "^6.10.0" @@ -4317,7 +4317,7 @@ eslint@^8.3.0: file-entry-cache "^6.0.1" find-up "^5.0.0" glob-parent "^6.0.2" - globals "^13.15.0" + globals "^13.19.0" grapheme-splitter "^1.0.4" ignore "^5.2.0" import-fresh "^3.0.0" @@ -4842,10 +4842,10 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.15.0: - version "13.18.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.18.0.tgz#fb224daeeb2bb7d254cd2c640f003528b8d0c1dc" - integrity sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A== +globals@^13.19.0: + version "13.19.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.19.0.tgz#7a42de8e6ad4f7242fbcca27ea5b23aca367b5c8" + integrity sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ== dependencies: type-fest "^0.20.2" @@ -5138,9 +5138,9 @@ identity-obj-proxy@^3.0.0: harmony-reflect "^1.4.6" ignore@^5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" - integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== immer@^9.0.7: version "9.0.16" @@ -5207,11 +5207,11 @@ inline-style-parser@0.1.1: integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== internal-slot@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" - integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + version "1.0.4" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" + integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== dependencies: - get-intrinsic "^1.1.0" + get-intrinsic "^1.1.3" has "^1.0.3" side-channel "^1.0.4" @@ -6193,9 +6193,9 @@ json5@^1.0.1: minimist "^1.2.0" json5@^2.1.2, json5@^2.2.0, json5@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" - integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + version "2.2.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.2.tgz#64471c5bdcc564c18f7c1d4df2e2297f2457c5ab" + integrity sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ== jsonfile@^6.0.1: version "6.1.0" @@ -6245,9 +6245,9 @@ language-subtag-registry@^0.3.20: integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== language-tags@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.6.tgz#c087cc42cd92eb71f0925e9e271d4f8be5a93430" - integrity sha512-HNkaCgM8wZgE/BZACeotAAgpL9FUjEnhgF0FVQMIgH//zqTPreLYMb3rWYkYAqPoF75Jwuycp1da7uz66cfFQg== + version "1.0.7" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.7.tgz#41cc248730f3f12a452c2e2efe32bc0bbce67967" + integrity sha512-bSytju1/657hFjgUzPAPqszxH62ouE8nQFoFaVlIQfne4wO/wXC9A4+m8jYve7YBBvi59eq0SUpcshvG8h5Usw== dependencies: language-subtag-registry "^0.3.20" @@ -6739,10 +6739,10 @@ minimalistic-assert@^1.0.0: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2, minimatch@^5.0.1, minimatch@^5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.1.tgz#6c9dffcf9927ff2a31e74b5af11adf8b9604b022" - integrity sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g== +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2, minimatch@^5.0.1, minimatch@^5.1.0, minimatch@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.2.tgz#0939d7d6f0898acbd1508abe534d1929368a8fff" + integrity sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg== dependencies: brace-expansion "^2.0.1" @@ -6842,9 +6842,9 @@ node-int64@^0.4.0: integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== node-releases@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" - integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + version "2.0.8" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae" + integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -7591,9 +7591,9 @@ postcss-normalize@^10.0.1: sanitize.css "*" postcss-opacity-percentage@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.2.tgz#bd698bb3670a0a27f6d657cc16744b3ebf3b1145" - integrity sha512-lyUfF7miG+yewZ8EAk9XUBIlrHyUE6fijnesuz+Mj5zrIHIEw6KcIZSOk/elVMqzLvREmXB83Zi/5QpNRYd47w== + version "1.1.3" + resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz#5b89b35551a556e20c5d23eb5260fbfcf5245da6" + integrity sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A== postcss-ordered-values@^5.1.3: version "5.1.3" @@ -7739,7 +7739,7 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^7.0.35, postcss@^8.2.13, postcss@^8.2.20, postcss@^8.3.5, postcss@^8.4.18, postcss@^8.4.4: +postcss@^7.0.35, postcss@^8.2.13, postcss@^8.2.20, postcss@^8.3.5, postcss@^8.4.18, postcss@^8.4.19, postcss@^8.4.4: version "8.4.20" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.20.tgz#64c52f509644cecad8567e949f4081d98349dc56" integrity sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g== @@ -8067,20 +8067,20 @@ 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-router-dom@^6.4.5: - version "6.4.5" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.4.5.tgz#4fdb12efef4f3848c693a76afbeaed1f6ca02047" - integrity sha512-a7HsgikBR0wNfroBHcZUCd9+mLRqZS8R5U1Z1mzLWxFXEkUT3vR1XXmSIVoVpxVX8Bar0nQYYYc9Yipq8dWwAA== +react-router-dom@^6.6.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.6.0.tgz#ad20b37b69e9fe7c6389663a0767ba40a19f71fe" + integrity sha512-qC4jnvpfCPKVle1mKLD75IvZLcbVJyFMlSn16WY9ZiOed3dgSmqhslCf/u3tmSccWOujkdsT/OwGq12bELmvjg== dependencies: - "@remix-run/router" "1.0.5" - react-router "6.4.5" + "@remix-run/router" "1.2.0" + react-router "6.6.0" -react-router@6.4.5: - version "6.4.5" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.4.5.tgz#73f382af2c8b9a86d74e761a7c5fc3ce7cb0024d" - integrity sha512-1RQJ8bM70YEumHIlNUYc6mFfUDoWa5EgPDenK/fq0bxD8DYpQUi/S6Zoft+9DBrh2xmtg92N5HMAJgGWDhKJ5Q== +react-router@6.6.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.6.0.tgz#9ae27cfc6bf7f2b28e1f028fe01fdbec96a3a41d" + integrity sha512-+VPfCIaFbkW7BAiB/2oeprxKAt1KLbl+zXZ10CXOYezKWgBmTKyh8XjI53eLqY5kd7uY+V4rh3UW44FclwUU+Q== dependencies: - "@remix-run/router" "1.0.5" + "@remix-run/router" "1.2.0" react-scripts@5.0.1: version "5.0.1" @@ -8476,10 +8476,10 @@ sass-loader@^12.3.0: klona "^2.0.4" neo-async "^2.6.2" -sass@^1.56.2: - version "1.56.2" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.2.tgz#9433b345ab3872996c82a53a58c014fd244fd095" - integrity sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w== +sass@^1.57.1: + version "1.57.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.57.1.tgz#dfafd46eb3ab94817145e8825208ecf7281119b5" + integrity sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -9212,10 +9212,10 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -tinycolor2@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" - integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA== +tinycolor2@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.5.1.tgz#df11c5f14e6b7fdd8a9c27c2c6a5f2565fb776b7" + integrity sha512-BHlrsGeYN2OpkRpfAgkEwCMu6w8Quq8JkK/mp4c55NZP7OwceJObR1CPZt62TqiA0Y3J5pwuDX+fXDqc35REtg== tmpl@1.0.5: version "1.0.5"