From 25ef91f22188fede214afa459744e1593f5013a6 Mon Sep 17 00:00:00 2001 From: Filipriec Date: Wed, 16 Sep 2026 16:09:55 +0200 Subject: [PATCH] better charts --- .cargo/config.toml | 3 ++ client-gui2 | 2 +- common/build.rs | 1 + common/proto/analytics.proto | 23 +++++++++ common/src/proto/descriptor.bin | Bin 308538 -> 309925 bytes common/src/proto/komp_ac.analytics.rs | 66 ++++++++++++++++++++++++++ common/src/reporting.rs | 27 +++++++++++ server | 2 +- tui-pages | 2 +- uniza_release.txt | 13 +++++ 10 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 uniza_release.txt diff --git a/.cargo/config.toml b/.cargo/config.toml index 41b5e65e..3e8d1938 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,6 @@ +[net] +offline = true + # Faster linking for local dev rebuilds. # Uses lld instead of the default bfd linker; requires `lld` from the flake # devShell. Run `direnv reload` after changing flake.nix. diff --git a/client-gui2 b/client-gui2 index 4cf770b9..09dd5432 160000 --- a/client-gui2 +++ b/client-gui2 @@ -1 +1 @@ -Subproject commit 4cf770b95619a4666b7d75c78001383c7c63ce06 +Subproject commit 09dd543243fc720b1737cb4fa950586c80949726 diff --git a/common/build.rs b/common/build.rs index be1b9b59..a8a51dd0 100644 --- a/common/build.rs +++ b/common/build.rs @@ -8,6 +8,7 @@ fn main() -> Result<(), Box> { // here makes the root common crate usable by TUI and Tauri alike. .type_attribute(".komp_ac.accounting", serde) .type_attribute(".komp_ac.analytics", serde) + .field_attribute(".komp_ac.analytics.ReportDashboardFilter.primary", "#[serde(default)]") .type_attribute(".komp_ac.auth", serde) .type_attribute(".komp_ac.common", serde) .type_attribute(".komp_ac.document_data", serde) diff --git a/common/proto/analytics.proto b/common/proto/analytics.proto index 5948d6a0..abc1b054 100644 --- a/common/proto/analytics.proto +++ b/common/proto/analytics.proto @@ -281,6 +281,28 @@ message ReportFilterTarget { message ReportDashboardFilter { ReportParameter parameter = 1; repeated ReportFilterTarget targets = 2; + bool primary = 3; +} + +enum ReportPeriodBoundary { + REPORT_PERIOD_BOUNDARY_UNSPECIFIED = 0; + REPORT_PERIOD_BOUNDARY_INCLUSIVE = 1; + REPORT_PERIOD_BOUNDARY_EXCLUSIVE = 2; +} + +message ReportDateRange { + string start_filter = 1; + string end_filter = 2; + ReportPeriodBoundary end_boundary = 3; +} + +message ReportPeriod { + string date_basis = 1; + oneof selection { + ReportDateRange date_range = 2; + string accounting_period_filter = 3; + string as_of_filter = 4; + } } message ReportActionBinding { @@ -353,6 +375,7 @@ message ReportDashboardDefinition { repeated ReportGrant grants = 5; uint32 refresh_seconds = 6; repeated ReportDatasetVersionRef dataset_versions = 7; + ReportPeriod period = 8; } message ReportDatasetVersionRef { diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index b4f761dd476eb072d068219ac4d9b82696bb3fb3..a95be341b2138c20769360c3daad071bb6204a70 100644 GIT binary patch delta 9490 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 E0Se?K>;M1& diff --git a/common/src/proto/komp_ac.analytics.rs b/common/src/proto/komp_ac.analytics.rs index ca207a73..482bc74b 100644 --- a/common/src/proto/komp_ac.analytics.rs +++ b/common/src/proto/komp_ac.analytics.rs @@ -252,6 +252,40 @@ pub struct ReportDashboardFilter { pub parameter: ::core::option::Option, #[prost(message, repeated, tag = "2")] pub targets: ::prost::alloc::vec::Vec, + #[prost(bool, tag = "3")] + #[serde(default)] + pub primary: bool, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportDateRange { + #[prost(string, tag = "1")] + pub start_filter: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub end_filter: ::prost::alloc::string::String, + #[prost(enumeration = "ReportPeriodBoundary", tag = "3")] + pub end_boundary: i32, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportPeriod { + #[prost(string, tag = "1")] + pub date_basis: ::prost::alloc::string::String, + #[prost(oneof = "report_period::Selection", tags = "2, 3, 4")] + pub selection: ::core::option::Option, +} +/// Nested message and enum types in `ReportPeriod`. +pub mod report_period { + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum Selection { + #[prost(message, tag = "2")] + DateRange(super::ReportDateRange), + #[prost(string, tag = "3")] + AccountingPeriodFilter(::prost::alloc::string::String), + #[prost(string, tag = "4")] + AsOfFilter(::prost::alloc::string::String), + } } #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] @@ -390,6 +424,8 @@ pub struct ReportDashboardDefinition { pub refresh_seconds: u32, #[prost(message, repeated, tag = "7")] pub dataset_versions: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "8")] + pub period: ::core::option::Option, } #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] @@ -1085,6 +1121,36 @@ impl ReportExecutionPurpose { } } } +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ReportPeriodBoundary { + Unspecified = 0, + Inclusive = 1, + Exclusive = 2, +} +impl ReportPeriodBoundary { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "REPORT_PERIOD_BOUNDARY_UNSPECIFIED", + Self::Inclusive => "REPORT_PERIOD_BOUNDARY_INCLUSIVE", + Self::Exclusive => "REPORT_PERIOD_BOUNDARY_EXCLUSIVE", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "REPORT_PERIOD_BOUNDARY_UNSPECIFIED" => Some(Self::Unspecified), + "REPORT_PERIOD_BOUNDARY_INCLUSIVE" => Some(Self::Inclusive), + "REPORT_PERIOD_BOUNDARY_EXCLUSIVE" => Some(Self::Exclusive), + _ => None, + } + } +} /// Generated client implementations. pub mod analytics_service_client { #![allow( diff --git a/common/src/reporting.rs b/common/src/reporting.rs index f3a2fb75..eca47544 100644 --- a/common/src/reporting.rs +++ b/common/src/reporting.rs @@ -263,6 +263,33 @@ fn validate_dashboard(dashboard: &ReportDashboardDefinition) -> Result<(), Strin } } } + if let Some(period) = &dashboard.period { + nonempty(&period.date_basis, "Period date basis", 160)?; + let parameter = |key: &str, kind: ReportDataType| -> Result<(), String> { + let parameter = dashboard.filters.iter().filter_map(|filter| filter.parameter.as_ref()) + .find(|parameter| parameter.key == key).ok_or("Period refers to an unknown filter")?; + let actual_kind: ReportDataType = enum_value(parameter.data_type, "period filter type")?; + if parameter.multiple || actual_kind != kind { + return Err("Period filters must have the declared single-value type".into()); + } + if !parameter.choices.is_empty() || parameter.lookup.is_some() { + return Err("Period filters use the period selector instead of custom choices".into()); + } + Ok(()) + }; + match period.selection.as_ref().ok_or("A period selection is required")? { + report_period::Selection::DateRange(range) => { + parameter(&range.start_filter, ReportDataType::Date)?; + parameter(&range.end_filter, ReportDataType::Date)?; + if range.start_filter == range.end_filter { + return Err("A period needs distinct start and end filters".into()); + } + let _: ReportPeriodBoundary = enum_value(range.end_boundary, "period end boundary")?; + } + report_period::Selection::AccountingPeriodFilter(key) => parameter(key, ReportDataType::Integer)?, + report_period::Selection::AsOfFilter(key) => parameter(key, ReportDataType::Date)?, + } + } let mut panels = HashSet::new(); for panel in &dashboard.panels { nonempty(&panel.id, "Panel ID", 64)?; diff --git a/server b/server index 27d28176..7efce3df 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 27d28176f06d3bdf67dd62d324a18303fae30279 +Subproject commit 7efce3df1842958d09b4501ebb5199b6807f127d diff --git a/tui-pages b/tui-pages index a135d67c..ce4bc49f 160000 --- a/tui-pages +++ b/tui-pages @@ -1 +1 @@ -Subproject commit a135d67c79f3f4562c994612014450ac2f0fbbfa +Subproject commit ce4bc49fa64225c8cc3ded9ce9c2e8e68cc32254 diff --git a/uniza_release.txt b/uniza_release.txt new file mode 100644 index 00000000..0aafced8 --- /dev/null +++ b/uniza_release.txt @@ -0,0 +1,13 @@ +tui-pages - MIT +MIT je licencia pomenovana po univerzite - kompatibilna s univerzitou +- kniznica pre pracu s terminalom a kniznicou ratatui(vyucovana tema) +- nadvazuje na hodiny a priamo vyucbu predmetu JR +- je v ramci existujuceho ekosystemu, ktory doplna + +Riadiaci a zaznamenavaju software - AGPL server GPL client +GPL je copyleft licencia, cize kod si nemoze firma prevziat do svojho proprietarneho systemu +- software je v podstate vysoko komplexny modularny system. +- optimalizovany na vykon, nizku latenci a vysoky objem dat +- nastavitelny, naprogramovatelny na lubovolne pouzitie +- Sluzi hlavne na vstup/vystup dat a ich analyzu +- Analyza umoznuje potencialnu optimalizaciu systemu, podvojnost, tvorbu vystupnych dokumentov, grafov apod.