From eb17094b2f58ac3c4df641d54463483371817e36 Mon Sep 17 00:00:00 2001 From: Priec Date: Wed, 16 Sep 2026 08:31:52 +0200 Subject: [PATCH] something --- client-gui2 | 2 +- common/proto/analytics.proto | 401 +++++ common/src/lib.rs | 1 + common/src/proto/descriptor.bin | Bin 282426 -> 308538 bytes common/src/proto/komp_ac.analytics.rs | 2042 +++++++++++++++++++++++++ common/src/reporting.rs | 550 +++++++ komp-app/src/analytics.rs | 249 +++ komp-app/src/grpc.rs | 1 + komp-app/src/grpc/reporting.rs | 134 ++ komp-app/src/lib.rs | 1 + 10 files changed, 3380 insertions(+), 1 deletion(-) create mode 100644 common/src/reporting.rs create mode 100644 komp-app/src/analytics.rs create mode 100644 komp-app/src/grpc/reporting.rs diff --git a/client-gui2 b/client-gui2 index 140708f7..e991c7fe 160000 --- a/client-gui2 +++ b/client-gui2 @@ -1 +1 @@ -Subproject commit 140708f7574f9f1a1efae229dd031699485290b0 +Subproject commit e991c7fec3104a0838a45179bc9728ed0fed46e7 diff --git a/common/proto/analytics.proto b/common/proto/analytics.proto index 2e890b71..5948d6a0 100644 --- a/common/proto/analytics.proto +++ b/common/proto/analytics.proto @@ -94,3 +94,404 @@ message AnalyticsResultBatch { uint64 row_count = 5; uint64 elapsed_ms = 6; } + +service ReportingService { + rpc ListAssets(ListReportAssetsRequest) returns (ListReportAssetsResponse); + rpc GetAsset(GetReportAssetRequest) returns (ReportAsset); + rpc SaveDraft(SaveReportDraftRequest) returns (ReportAsset); + rpc Publish(PublishReportRequest) returns (ReportAsset); + rpc ListVersions(ReportAssetRef) returns (ListReportVersionsResponse); + rpc RestoreDraft(RestoreReportDraftRequest) returns (ReportAsset); + rpc SetArchived(SetReportArchivedRequest) returns (ReportAsset); + rpc ExecuteDataset(ExecuteReportDatasetRequest) returns (stream AnalyticsResultBatch); + rpc GetPersonalViews(ReportAssetRef) returns (GetReportPersonalViewsResponse); + rpc SavePersonalView(SaveReportPersonalViewRequest) returns (ReportPersonalView); + rpc DeletePersonalView(DeleteReportPersonalViewRequest) returns (ReportMutationResult); +} + +enum ReportAssetKind { + REPORT_ASSET_KIND_UNSPECIFIED = 0; + REPORT_ASSET_KIND_DATASET = 1; + REPORT_ASSET_KIND_DASHBOARD = 2; +} + +enum ReportReadMode { + REPORT_READ_MODE_UNSPECIFIED = 0; + REPORT_READ_MODE_PUBLISHED = 1; + REPORT_READ_MODE_DRAFT = 2; + REPORT_READ_MODE_VERSION = 3; +} + +enum ReportDataType { + REPORT_DATA_TYPE_UNSPECIFIED = 0; + REPORT_DATA_TYPE_TEXT = 1; + REPORT_DATA_TYPE_INTEGER = 2; + REPORT_DATA_TYPE_DECIMAL = 3; + REPORT_DATA_TYPE_DATE = 4; + REPORT_DATA_TYPE_TIMESTAMP = 5; + REPORT_DATA_TYPE_BOOLEAN = 6; +} + +enum ReportFilterControl { + REPORT_FILTER_CONTROL_UNSPECIFIED = 0; + REPORT_FILTER_CONTROL_TEXT = 1; + REPORT_FILTER_CONTROL_NUMBER = 2; + REPORT_FILTER_CONTROL_DATE = 3; + REPORT_FILTER_CONTROL_TIMESTAMP = 4; + REPORT_FILTER_CONTROL_SELECT = 5; + REPORT_FILTER_CONTROL_MULTISELECT = 6; + REPORT_FILTER_CONTROL_CHECKBOX = 7; +} + +enum ReportDefaultKind { + REPORT_DEFAULT_KIND_UNSPECIFIED = 0; + REPORT_DEFAULT_KIND_LITERAL = 1; + REPORT_DEFAULT_KIND_TODAY = 2; + REPORT_DEFAULT_KIND_MONTH_START = 3; + REPORT_DEFAULT_KIND_YEAR_START = 4; +} + +enum ReportNumberFormat { + REPORT_NUMBER_FORMAT_UNSPECIFIED = 0; + REPORT_NUMBER_FORMAT_NUMBER = 1; + REPORT_NUMBER_FORMAT_CURRENCY = 2; + REPORT_NUMBER_FORMAT_PERCENT = 3; + REPORT_NUMBER_FORMAT_COMPACT = 4; +} + +enum ReportPanelKind { + REPORT_PANEL_KIND_UNSPECIFIED = 0; + REPORT_PANEL_KIND_BAR = 1; + REPORT_PANEL_KIND_LINE = 2; + REPORT_PANEL_KIND_AREA = 3; + REPORT_PANEL_KIND_PIE = 4; + REPORT_PANEL_KIND_DONUT = 5; + REPORT_PANEL_KIND_SCATTER = 6; + REPORT_PANEL_KIND_HEATMAP = 7; + REPORT_PANEL_KIND_TREEMAP = 8; + REPORT_PANEL_KIND_FUNNEL = 9; + REPORT_PANEL_KIND_GAUGE = 10; + REPORT_PANEL_KIND_WATERFALL = 11; + REPORT_PANEL_KIND_TABLE = 12; + REPORT_PANEL_KIND_KPI = 13; +} + +enum ReportOrientation { + REPORT_ORIENTATION_UNSPECIFIED = 0; + REPORT_ORIENTATION_VERTICAL = 1; + REPORT_ORIENTATION_HORIZONTAL = 2; +} + +enum ReportSortOrder { + REPORT_SORT_ORDER_UNSPECIFIED = 0; + REPORT_SORT_ORDER_SOURCE = 1; + REPORT_SORT_ORDER_ASCENDING = 2; + REPORT_SORT_ORDER_DESCENDING = 3; +} + +enum ReportNullPolicy { + REPORT_NULL_POLICY_UNSPECIFIED = 0; + REPORT_NULL_POLICY_GAP = 1; + REPORT_NULL_POLICY_ZERO = 2; +} + +enum ReportCapability { + REPORT_CAPABILITY_UNSPECIFIED = 0; + REPORT_CAPABILITY_VIEW = 1; + REPORT_CAPABILITY_FILTER = 2; + REPORT_CAPABILITY_DRILL = 3; + REPORT_CAPABILITY_EXPORT = 4; + REPORT_CAPABILITY_CUSTOMIZE = 5; +} + +enum ReportExecutionPurpose { + REPORT_EXECUTION_PURPOSE_UNSPECIFIED = 0; + REPORT_EXECUTION_PURPOSE_VIEW = 1; + REPORT_EXECUTION_PURPOSE_PREVIEW = 2; + REPORT_EXECUTION_PURPOSE_EXPORT = 3; + REPORT_EXECUTION_PURPOSE_DRILL = 4; +} + +message ReportScalar { + oneof value { + google.protobuf.NullValue null_value = 1; + string text = 2; + string integer = 3; + string decimal = 4; + string date = 5; + string timestamp = 6; + bool boolean = 7; + } +} + +message ReportParameterChoice { + string label = 1; + ReportScalar value = 2; +} + +message ReportParameterLookup { + string dataset_id = 1; + string value_field = 2; + string label_field = 3; +} + +message ReportParameter { + string key = 1; + string label = 2; + ReportDataType data_type = 3; + ReportFilterControl control = 4; + bool required = 5; + bool multiple = 6; + repeated ReportScalar default_values = 7; + ReportDefaultKind default_kind = 8; + repeated ReportParameterChoice choices = 9; + ReportParameterLookup lookup = 10; +} + +message ReportParameterBinding { + string key = 1; + repeated ReportScalar values = 2; +} + +message ReportDatasetColumn { + string key = 1; + string label = 2; + ReportDataType data_type = 3; + ReportNumberFormat number_format = 4; + string currency = 5; + string currency_field = 6; + optional uint32 fraction_digits = 7; +} + +message ReportDatasetDefinition { + string title = 1; + string description = 2; + // Parameters use named DataFusion placeholders, for example CAST($from AS DATE). + string sql = 3; + repeated ReportParameter parameters = 4; + repeated ReportDatasetColumn columns = 5; + uint32 max_rows = 6; +} + +message ReportFilterTarget { + string dataset_id = 1; + string parameter_key = 2; +} + +message ReportDashboardFilter { + ReportParameter parameter = 1; + repeated ReportFilterTarget targets = 2; +} + +message ReportActionBinding { + string filter_key = 1; + string column_key = 2; +} + +message ReportFilterAction { + repeated ReportActionBinding bindings = 1; +} + +message ReportRecordAction { + string table_name = 1; + string id_field = 2; +} + +message ReportDashboardAction { + string dashboard_id = 1; + repeated ReportActionBinding bindings = 2; +} + +message ReportPanelAction { + string label = 1; + oneof target { + ReportFilterAction filter = 2; + ReportRecordAction record = 3; + ReportDashboardAction dashboard = 4; + } +} + +message ReportPanel { + string id = 1; + string title = 2; + string description = 3; + string dataset_id = 4; + ReportPanelKind kind = 5; + string x_field = 6; + repeated string y_fields = 7; + string series_field = 8; + string size_field = 9; + repeated string table_fields = 10; + ReportOrientation orientation = 11; + bool stacked = 12; + bool show_legend = 13; + bool show_labels = 14; + uint32 width = 15; + uint32 height = 16; + repeated string colors = 17; + ReportSortOrder sort_order = 18; + string sort_field = 19; + ReportNullPolicy null_policy = 20; + optional double axis_min = 21; + optional double axis_max = 22; + repeated ReportPanelAction actions = 23; +} + +message ReportGrant { + oneof subject { + string role = 1; + string user_id = 2; + } + repeated ReportCapability capabilities = 3; +} + +message ReportDashboardDefinition { + string title = 1; + string description = 2; + repeated ReportDashboardFilter filters = 3; + repeated ReportPanel panels = 4; + repeated ReportGrant grants = 5; + uint32 refresh_seconds = 6; + repeated ReportDatasetVersionRef dataset_versions = 7; +} + +message ReportDatasetVersionRef { + string dataset_id = 1; + // Zero selects the latest published dataset when the dashboard is published. + uint64 version = 2; +} + +message ReportDefinition { + uint32 schema_version = 1; + oneof content { + ReportDatasetDefinition dataset = 2; + ReportDashboardDefinition dashboard = 3; + } +} + +message ReportDatasetSnapshot { + string dataset_id = 1; + uint64 version = 2; + ReportDatasetDefinition definition = 3; +} + +message ReportAssetRef { + string profile_name = 1; + string asset_id = 2; +} + +message ReportAssetSummary { + string id = 1; + string title = 2; + string description = 3; + ReportAssetKind kind = 4; + uint64 draft_revision = 5; + uint64 published_version = 6; + bool archived = 7; + string updated_at = 8; + repeated ReportCapability capabilities = 9; +} + +message ReportAsset { + ReportAssetSummary summary = 1; + ReportDefinition definition = 2; + repeated ReportDatasetSnapshot datasets = 3; + string profile_name = 4; + uint64 version = 5; +} + +message ListReportAssetsRequest { + string profile_name = 1; + bool include_archived = 2; +} + +message ListReportAssetsResponse { + repeated ReportAssetSummary assets = 1; + bool can_manage = 2; +} + +message GetReportAssetRequest { + ReportAssetRef asset = 1; + ReportReadMode mode = 2; + uint64 version = 3; +} + +message SaveReportDraftRequest { + ReportAssetRef asset = 1; + uint64 expected_revision = 2; + ReportDefinition definition = 3; +} + +message PublishReportRequest { + ReportAssetRef asset = 1; + uint64 expected_revision = 2; +} + +message ReportVersionSummary { + uint64 version = 1; + string title = 2; + string created_at = 3; + string created_by = 4; +} + +message ListReportVersionsResponse { + repeated ReportVersionSummary versions = 1; +} + +message RestoreReportDraftRequest { + ReportAssetRef asset = 1; + uint64 expected_revision = 2; + uint64 version = 3; +} + +message SetReportArchivedRequest { + ReportAssetRef asset = 1; + uint64 expected_revision = 2; + bool archived = 3; +} + +message ExecuteReportDatasetRequest { + ReportAssetRef asset = 1; + // Published version for viewer requests; draft revision for previews. + uint64 version = 2; + string dataset_id = 3; + repeated ReportParameterBinding filters = 4; + ReportExecutionPurpose purpose = 5; + string panel_id = 6; + uint32 action_index = 7; +} + +message ReportPanelPreference { + string panel_id = 1; + uint32 width = 2; + uint32 height = 3; + bool hidden = 4; +} + +message ReportPersonalView { + string id = 1; + string title = 2; + uint64 report_version = 3; + repeated ReportParameterBinding filters = 4; + repeated ReportPanelPreference panels = 5; + uint64 revision = 6; +} + +message GetReportPersonalViewsResponse { + repeated ReportPersonalView views = 1; +} + +message SaveReportPersonalViewRequest { + ReportAssetRef asset = 1; + ReportPersonalView view = 2; +} + +message DeleteReportPersonalViewRequest { + ReportAssetRef asset = 1; + string view_id = 2; + uint64 expected_revision = 3; +} + +message ReportMutationResult { + bool success = 1; +} diff --git a/common/src/lib.rs b/common/src/lib.rs index da5a8ade..a15ad7cf 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -8,6 +8,7 @@ pub mod decimal; pub mod grpc_error; pub mod money; pub mod relationship; +pub mod reporting; pub mod script_operations; pub mod system_column; pub mod typst_contract; diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index d8e28487a061cc02006d6101c3e34674da3a0e53..b4f761dd476eb072d068219ac4d9b82696bb3fb3 100644 GIT binary patch delta 24744 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 delta 59 zcmV-B0L1^g>Jqx15rBjNv;s?R0?jG6PHqBy2B$il0s#t2#Tp6*5(EGMUzhMT0u;Ba Rp#mxmhZ5-mw-V_CD9mjV6ZZfB diff --git a/common/src/proto/komp_ac.analytics.rs b/common/src/proto/komp_ac.analytics.rs index 989ebb7b..ca207a73 100644 --- a/common/src/proto/komp_ac.analytics.rs +++ b/common/src/proto/komp_ac.analytics.rs @@ -125,6 +125,966 @@ pub struct AnalyticsResultBatch { #[prost(uint64, tag = "6")] pub elapsed_ms: u64, } +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportScalar { + #[prost(oneof = "report_scalar::Value", tags = "1, 2, 3, 4, 5, 6, 7")] + pub value: ::core::option::Option, +} +/// Nested message and enum types in `ReportScalar`. +pub mod report_scalar { + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum Value { + #[prost(enumeration = "::prost_types::NullValue", tag = "1")] + NullValue(i32), + #[prost(string, tag = "2")] + Text(::prost::alloc::string::String), + #[prost(string, tag = "3")] + Integer(::prost::alloc::string::String), + #[prost(string, tag = "4")] + Decimal(::prost::alloc::string::String), + #[prost(string, tag = "5")] + Date(::prost::alloc::string::String), + #[prost(string, tag = "6")] + Timestamp(::prost::alloc::string::String), + #[prost(bool, tag = "7")] + Boolean(bool), + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportParameterChoice { + #[prost(string, tag = "1")] + pub label: ::prost::alloc::string::String, + #[prost(message, optional, tag = "2")] + pub value: ::core::option::Option, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportParameterLookup { + #[prost(string, tag = "1")] + pub dataset_id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub value_field: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub label_field: ::prost::alloc::string::String, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportParameter { + #[prost(string, tag = "1")] + pub key: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub label: ::prost::alloc::string::String, + #[prost(enumeration = "ReportDataType", tag = "3")] + pub data_type: i32, + #[prost(enumeration = "ReportFilterControl", tag = "4")] + pub control: i32, + #[prost(bool, tag = "5")] + pub required: bool, + #[prost(bool, tag = "6")] + pub multiple: bool, + #[prost(message, repeated, tag = "7")] + pub default_values: ::prost::alloc::vec::Vec, + #[prost(enumeration = "ReportDefaultKind", tag = "8")] + pub default_kind: i32, + #[prost(message, repeated, tag = "9")] + pub choices: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "10")] + pub lookup: ::core::option::Option, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportParameterBinding { + #[prost(string, tag = "1")] + pub key: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "2")] + pub values: ::prost::alloc::vec::Vec, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportDatasetColumn { + #[prost(string, tag = "1")] + pub key: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub label: ::prost::alloc::string::String, + #[prost(enumeration = "ReportDataType", tag = "3")] + pub data_type: i32, + #[prost(enumeration = "ReportNumberFormat", tag = "4")] + pub number_format: i32, + #[prost(string, tag = "5")] + pub currency: ::prost::alloc::string::String, + #[prost(string, tag = "6")] + pub currency_field: ::prost::alloc::string::String, + #[prost(uint32, optional, tag = "7")] + pub fraction_digits: ::core::option::Option, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportDatasetDefinition { + #[prost(string, tag = "1")] + pub title: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub description: ::prost::alloc::string::String, + /// Parameters use named DataFusion placeholders, for example CAST($from AS DATE). + #[prost(string, tag = "3")] + pub sql: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "4")] + pub parameters: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "5")] + pub columns: ::prost::alloc::vec::Vec, + #[prost(uint32, tag = "6")] + pub max_rows: u32, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportFilterTarget { + #[prost(string, tag = "1")] + pub dataset_id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub parameter_key: ::prost::alloc::string::String, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportDashboardFilter { + #[prost(message, optional, tag = "1")] + pub parameter: ::core::option::Option, + #[prost(message, repeated, tag = "2")] + pub targets: ::prost::alloc::vec::Vec, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportActionBinding { + #[prost(string, tag = "1")] + pub filter_key: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub column_key: ::prost::alloc::string::String, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportFilterAction { + #[prost(message, repeated, tag = "1")] + pub bindings: ::prost::alloc::vec::Vec, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportRecordAction { + #[prost(string, tag = "1")] + pub table_name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub id_field: ::prost::alloc::string::String, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportDashboardAction { + #[prost(string, tag = "1")] + pub dashboard_id: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "2")] + pub bindings: ::prost::alloc::vec::Vec, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportPanelAction { + #[prost(string, tag = "1")] + pub label: ::prost::alloc::string::String, + #[prost(oneof = "report_panel_action::Target", tags = "2, 3, 4")] + pub target: ::core::option::Option, +} +/// Nested message and enum types in `ReportPanelAction`. +pub mod report_panel_action { + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Target { + #[prost(message, tag = "2")] + Filter(super::ReportFilterAction), + #[prost(message, tag = "3")] + Record(super::ReportRecordAction), + #[prost(message, tag = "4")] + Dashboard(super::ReportDashboardAction), + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportPanel { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub title: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub description: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub dataset_id: ::prost::alloc::string::String, + #[prost(enumeration = "ReportPanelKind", tag = "5")] + pub kind: i32, + #[prost(string, tag = "6")] + pub x_field: ::prost::alloc::string::String, + #[prost(string, repeated, tag = "7")] + pub y_fields: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(string, tag = "8")] + pub series_field: ::prost::alloc::string::String, + #[prost(string, tag = "9")] + pub size_field: ::prost::alloc::string::String, + #[prost(string, repeated, tag = "10")] + pub table_fields: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(enumeration = "ReportOrientation", tag = "11")] + pub orientation: i32, + #[prost(bool, tag = "12")] + pub stacked: bool, + #[prost(bool, tag = "13")] + pub show_legend: bool, + #[prost(bool, tag = "14")] + pub show_labels: bool, + #[prost(uint32, tag = "15")] + pub width: u32, + #[prost(uint32, tag = "16")] + pub height: u32, + #[prost(string, repeated, tag = "17")] + pub colors: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(enumeration = "ReportSortOrder", tag = "18")] + pub sort_order: i32, + #[prost(string, tag = "19")] + pub sort_field: ::prost::alloc::string::String, + #[prost(enumeration = "ReportNullPolicy", tag = "20")] + pub null_policy: i32, + #[prost(double, optional, tag = "21")] + pub axis_min: ::core::option::Option, + #[prost(double, optional, tag = "22")] + pub axis_max: ::core::option::Option, + #[prost(message, repeated, tag = "23")] + pub actions: ::prost::alloc::vec::Vec, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportGrant { + #[prost(enumeration = "ReportCapability", repeated, tag = "3")] + pub capabilities: ::prost::alloc::vec::Vec, + #[prost(oneof = "report_grant::Subject", tags = "1, 2")] + pub subject: ::core::option::Option, +} +/// Nested message and enum types in `ReportGrant`. +pub mod report_grant { + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum Subject { + #[prost(string, tag = "1")] + Role(::prost::alloc::string::String), + #[prost(string, tag = "2")] + UserId(::prost::alloc::string::String), + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportDashboardDefinition { + #[prost(string, tag = "1")] + pub title: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub description: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "3")] + pub filters: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "4")] + pub panels: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "5")] + pub grants: ::prost::alloc::vec::Vec, + #[prost(uint32, tag = "6")] + pub refresh_seconds: u32, + #[prost(message, repeated, tag = "7")] + pub dataset_versions: ::prost::alloc::vec::Vec, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportDatasetVersionRef { + #[prost(string, tag = "1")] + pub dataset_id: ::prost::alloc::string::String, + /// Zero selects the latest published dataset when the dashboard is published. + #[prost(uint64, tag = "2")] + pub version: u64, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportDefinition { + #[prost(uint32, tag = "1")] + pub schema_version: u32, + #[prost(oneof = "report_definition::Content", tags = "2, 3")] + pub content: ::core::option::Option, +} +/// Nested message and enum types in `ReportDefinition`. +pub mod report_definition { + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Content { + #[prost(message, tag = "2")] + Dataset(super::ReportDatasetDefinition), + #[prost(message, tag = "3")] + Dashboard(super::ReportDashboardDefinition), + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportDatasetSnapshot { + #[prost(string, tag = "1")] + pub dataset_id: ::prost::alloc::string::String, + #[prost(uint64, tag = "2")] + pub version: u64, + #[prost(message, optional, tag = "3")] + pub definition: ::core::option::Option, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportAssetRef { + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub asset_id: ::prost::alloc::string::String, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportAssetSummary { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub title: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub description: ::prost::alloc::string::String, + #[prost(enumeration = "ReportAssetKind", tag = "4")] + pub kind: i32, + #[prost(uint64, tag = "5")] + pub draft_revision: u64, + #[prost(uint64, tag = "6")] + pub published_version: u64, + #[prost(bool, tag = "7")] + pub archived: bool, + #[prost(string, tag = "8")] + pub updated_at: ::prost::alloc::string::String, + #[prost(enumeration = "ReportCapability", repeated, tag = "9")] + pub capabilities: ::prost::alloc::vec::Vec, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportAsset { + #[prost(message, optional, tag = "1")] + pub summary: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub definition: ::core::option::Option, + #[prost(message, repeated, tag = "3")] + pub datasets: ::prost::alloc::vec::Vec, + #[prost(string, tag = "4")] + pub profile_name: ::prost::alloc::string::String, + #[prost(uint64, tag = "5")] + pub version: u64, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ListReportAssetsRequest { + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, + #[prost(bool, tag = "2")] + pub include_archived: bool, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListReportAssetsResponse { + #[prost(message, repeated, tag = "1")] + pub assets: ::prost::alloc::vec::Vec, + #[prost(bool, tag = "2")] + pub can_manage: bool, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct GetReportAssetRequest { + #[prost(message, optional, tag = "1")] + pub asset: ::core::option::Option, + #[prost(enumeration = "ReportReadMode", tag = "2")] + pub mode: i32, + #[prost(uint64, tag = "3")] + pub version: u64, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SaveReportDraftRequest { + #[prost(message, optional, tag = "1")] + pub asset: ::core::option::Option, + #[prost(uint64, tag = "2")] + pub expected_revision: u64, + #[prost(message, optional, tag = "3")] + pub definition: ::core::option::Option, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct PublishReportRequest { + #[prost(message, optional, tag = "1")] + pub asset: ::core::option::Option, + #[prost(uint64, tag = "2")] + pub expected_revision: u64, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportVersionSummary { + #[prost(uint64, tag = "1")] + pub version: u64, + #[prost(string, tag = "2")] + pub title: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub created_at: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub created_by: ::prost::alloc::string::String, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListReportVersionsResponse { + #[prost(message, repeated, tag = "1")] + pub versions: ::prost::alloc::vec::Vec, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct RestoreReportDraftRequest { + #[prost(message, optional, tag = "1")] + pub asset: ::core::option::Option, + #[prost(uint64, tag = "2")] + pub expected_revision: u64, + #[prost(uint64, tag = "3")] + pub version: u64, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct SetReportArchivedRequest { + #[prost(message, optional, tag = "1")] + pub asset: ::core::option::Option, + #[prost(uint64, tag = "2")] + pub expected_revision: u64, + #[prost(bool, tag = "3")] + pub archived: bool, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExecuteReportDatasetRequest { + #[prost(message, optional, tag = "1")] + pub asset: ::core::option::Option, + /// Published version for viewer requests; draft revision for previews. + #[prost(uint64, tag = "2")] + pub version: u64, + #[prost(string, tag = "3")] + pub dataset_id: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "4")] + pub filters: ::prost::alloc::vec::Vec, + #[prost(enumeration = "ReportExecutionPurpose", tag = "5")] + pub purpose: i32, + #[prost(string, tag = "6")] + pub panel_id: ::prost::alloc::string::String, + #[prost(uint32, tag = "7")] + pub action_index: u32, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportPanelPreference { + #[prost(string, tag = "1")] + pub panel_id: ::prost::alloc::string::String, + #[prost(uint32, tag = "2")] + pub width: u32, + #[prost(uint32, tag = "3")] + pub height: u32, + #[prost(bool, tag = "4")] + pub hidden: bool, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportPersonalView { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub title: ::prost::alloc::string::String, + #[prost(uint64, tag = "3")] + pub report_version: u64, + #[prost(message, repeated, tag = "4")] + pub filters: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "5")] + pub panels: ::prost::alloc::vec::Vec, + #[prost(uint64, tag = "6")] + pub revision: u64, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetReportPersonalViewsResponse { + #[prost(message, repeated, tag = "1")] + pub views: ::prost::alloc::vec::Vec, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SaveReportPersonalViewRequest { + #[prost(message, optional, tag = "1")] + pub asset: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub view: ::core::option::Option, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct DeleteReportPersonalViewRequest { + #[prost(message, optional, tag = "1")] + pub asset: ::core::option::Option, + #[prost(string, tag = "2")] + pub view_id: ::prost::alloc::string::String, + #[prost(uint64, tag = "3")] + pub expected_revision: u64, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReportMutationResult { + #[prost(bool, tag = "1")] + pub success: bool, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ReportAssetKind { + Unspecified = 0, + Dataset = 1, + Dashboard = 2, +} +impl ReportAssetKind { + /// 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_ASSET_KIND_UNSPECIFIED", + Self::Dataset => "REPORT_ASSET_KIND_DATASET", + Self::Dashboard => "REPORT_ASSET_KIND_DASHBOARD", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "REPORT_ASSET_KIND_UNSPECIFIED" => Some(Self::Unspecified), + "REPORT_ASSET_KIND_DATASET" => Some(Self::Dataset), + "REPORT_ASSET_KIND_DASHBOARD" => Some(Self::Dashboard), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ReportReadMode { + Unspecified = 0, + Published = 1, + Draft = 2, + Version = 3, +} +impl ReportReadMode { + /// 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_READ_MODE_UNSPECIFIED", + Self::Published => "REPORT_READ_MODE_PUBLISHED", + Self::Draft => "REPORT_READ_MODE_DRAFT", + Self::Version => "REPORT_READ_MODE_VERSION", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "REPORT_READ_MODE_UNSPECIFIED" => Some(Self::Unspecified), + "REPORT_READ_MODE_PUBLISHED" => Some(Self::Published), + "REPORT_READ_MODE_DRAFT" => Some(Self::Draft), + "REPORT_READ_MODE_VERSION" => Some(Self::Version), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ReportDataType { + Unspecified = 0, + Text = 1, + Integer = 2, + Decimal = 3, + Date = 4, + Timestamp = 5, + Boolean = 6, +} +impl ReportDataType { + /// 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_DATA_TYPE_UNSPECIFIED", + Self::Text => "REPORT_DATA_TYPE_TEXT", + Self::Integer => "REPORT_DATA_TYPE_INTEGER", + Self::Decimal => "REPORT_DATA_TYPE_DECIMAL", + Self::Date => "REPORT_DATA_TYPE_DATE", + Self::Timestamp => "REPORT_DATA_TYPE_TIMESTAMP", + Self::Boolean => "REPORT_DATA_TYPE_BOOLEAN", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "REPORT_DATA_TYPE_UNSPECIFIED" => Some(Self::Unspecified), + "REPORT_DATA_TYPE_TEXT" => Some(Self::Text), + "REPORT_DATA_TYPE_INTEGER" => Some(Self::Integer), + "REPORT_DATA_TYPE_DECIMAL" => Some(Self::Decimal), + "REPORT_DATA_TYPE_DATE" => Some(Self::Date), + "REPORT_DATA_TYPE_TIMESTAMP" => Some(Self::Timestamp), + "REPORT_DATA_TYPE_BOOLEAN" => Some(Self::Boolean), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ReportFilterControl { + Unspecified = 0, + Text = 1, + Number = 2, + Date = 3, + Timestamp = 4, + Select = 5, + Multiselect = 6, + Checkbox = 7, +} +impl ReportFilterControl { + /// 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_FILTER_CONTROL_UNSPECIFIED", + Self::Text => "REPORT_FILTER_CONTROL_TEXT", + Self::Number => "REPORT_FILTER_CONTROL_NUMBER", + Self::Date => "REPORT_FILTER_CONTROL_DATE", + Self::Timestamp => "REPORT_FILTER_CONTROL_TIMESTAMP", + Self::Select => "REPORT_FILTER_CONTROL_SELECT", + Self::Multiselect => "REPORT_FILTER_CONTROL_MULTISELECT", + Self::Checkbox => "REPORT_FILTER_CONTROL_CHECKBOX", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "REPORT_FILTER_CONTROL_UNSPECIFIED" => Some(Self::Unspecified), + "REPORT_FILTER_CONTROL_TEXT" => Some(Self::Text), + "REPORT_FILTER_CONTROL_NUMBER" => Some(Self::Number), + "REPORT_FILTER_CONTROL_DATE" => Some(Self::Date), + "REPORT_FILTER_CONTROL_TIMESTAMP" => Some(Self::Timestamp), + "REPORT_FILTER_CONTROL_SELECT" => Some(Self::Select), + "REPORT_FILTER_CONTROL_MULTISELECT" => Some(Self::Multiselect), + "REPORT_FILTER_CONTROL_CHECKBOX" => Some(Self::Checkbox), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ReportDefaultKind { + Unspecified = 0, + Literal = 1, + Today = 2, + MonthStart = 3, + YearStart = 4, +} +impl ReportDefaultKind { + /// 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_DEFAULT_KIND_UNSPECIFIED", + Self::Literal => "REPORT_DEFAULT_KIND_LITERAL", + Self::Today => "REPORT_DEFAULT_KIND_TODAY", + Self::MonthStart => "REPORT_DEFAULT_KIND_MONTH_START", + Self::YearStart => "REPORT_DEFAULT_KIND_YEAR_START", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "REPORT_DEFAULT_KIND_UNSPECIFIED" => Some(Self::Unspecified), + "REPORT_DEFAULT_KIND_LITERAL" => Some(Self::Literal), + "REPORT_DEFAULT_KIND_TODAY" => Some(Self::Today), + "REPORT_DEFAULT_KIND_MONTH_START" => Some(Self::MonthStart), + "REPORT_DEFAULT_KIND_YEAR_START" => Some(Self::YearStart), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ReportNumberFormat { + Unspecified = 0, + Number = 1, + Currency = 2, + Percent = 3, + Compact = 4, +} +impl ReportNumberFormat { + /// 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_NUMBER_FORMAT_UNSPECIFIED", + Self::Number => "REPORT_NUMBER_FORMAT_NUMBER", + Self::Currency => "REPORT_NUMBER_FORMAT_CURRENCY", + Self::Percent => "REPORT_NUMBER_FORMAT_PERCENT", + Self::Compact => "REPORT_NUMBER_FORMAT_COMPACT", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "REPORT_NUMBER_FORMAT_UNSPECIFIED" => Some(Self::Unspecified), + "REPORT_NUMBER_FORMAT_NUMBER" => Some(Self::Number), + "REPORT_NUMBER_FORMAT_CURRENCY" => Some(Self::Currency), + "REPORT_NUMBER_FORMAT_PERCENT" => Some(Self::Percent), + "REPORT_NUMBER_FORMAT_COMPACT" => Some(Self::Compact), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ReportPanelKind { + Unspecified = 0, + Bar = 1, + Line = 2, + Area = 3, + Pie = 4, + Donut = 5, + Scatter = 6, + Heatmap = 7, + Treemap = 8, + Funnel = 9, + Gauge = 10, + Waterfall = 11, + Table = 12, + Kpi = 13, +} +impl ReportPanelKind { + /// 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_PANEL_KIND_UNSPECIFIED", + Self::Bar => "REPORT_PANEL_KIND_BAR", + Self::Line => "REPORT_PANEL_KIND_LINE", + Self::Area => "REPORT_PANEL_KIND_AREA", + Self::Pie => "REPORT_PANEL_KIND_PIE", + Self::Donut => "REPORT_PANEL_KIND_DONUT", + Self::Scatter => "REPORT_PANEL_KIND_SCATTER", + Self::Heatmap => "REPORT_PANEL_KIND_HEATMAP", + Self::Treemap => "REPORT_PANEL_KIND_TREEMAP", + Self::Funnel => "REPORT_PANEL_KIND_FUNNEL", + Self::Gauge => "REPORT_PANEL_KIND_GAUGE", + Self::Waterfall => "REPORT_PANEL_KIND_WATERFALL", + Self::Table => "REPORT_PANEL_KIND_TABLE", + Self::Kpi => "REPORT_PANEL_KIND_KPI", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "REPORT_PANEL_KIND_UNSPECIFIED" => Some(Self::Unspecified), + "REPORT_PANEL_KIND_BAR" => Some(Self::Bar), + "REPORT_PANEL_KIND_LINE" => Some(Self::Line), + "REPORT_PANEL_KIND_AREA" => Some(Self::Area), + "REPORT_PANEL_KIND_PIE" => Some(Self::Pie), + "REPORT_PANEL_KIND_DONUT" => Some(Self::Donut), + "REPORT_PANEL_KIND_SCATTER" => Some(Self::Scatter), + "REPORT_PANEL_KIND_HEATMAP" => Some(Self::Heatmap), + "REPORT_PANEL_KIND_TREEMAP" => Some(Self::Treemap), + "REPORT_PANEL_KIND_FUNNEL" => Some(Self::Funnel), + "REPORT_PANEL_KIND_GAUGE" => Some(Self::Gauge), + "REPORT_PANEL_KIND_WATERFALL" => Some(Self::Waterfall), + "REPORT_PANEL_KIND_TABLE" => Some(Self::Table), + "REPORT_PANEL_KIND_KPI" => Some(Self::Kpi), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ReportOrientation { + Unspecified = 0, + Vertical = 1, + Horizontal = 2, +} +impl ReportOrientation { + /// 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_ORIENTATION_UNSPECIFIED", + Self::Vertical => "REPORT_ORIENTATION_VERTICAL", + Self::Horizontal => "REPORT_ORIENTATION_HORIZONTAL", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "REPORT_ORIENTATION_UNSPECIFIED" => Some(Self::Unspecified), + "REPORT_ORIENTATION_VERTICAL" => Some(Self::Vertical), + "REPORT_ORIENTATION_HORIZONTAL" => Some(Self::Horizontal), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ReportSortOrder { + Unspecified = 0, + Source = 1, + Ascending = 2, + Descending = 3, +} +impl ReportSortOrder { + /// 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_SORT_ORDER_UNSPECIFIED", + Self::Source => "REPORT_SORT_ORDER_SOURCE", + Self::Ascending => "REPORT_SORT_ORDER_ASCENDING", + Self::Descending => "REPORT_SORT_ORDER_DESCENDING", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "REPORT_SORT_ORDER_UNSPECIFIED" => Some(Self::Unspecified), + "REPORT_SORT_ORDER_SOURCE" => Some(Self::Source), + "REPORT_SORT_ORDER_ASCENDING" => Some(Self::Ascending), + "REPORT_SORT_ORDER_DESCENDING" => Some(Self::Descending), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ReportNullPolicy { + Unspecified = 0, + Gap = 1, + Zero = 2, +} +impl ReportNullPolicy { + /// 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_NULL_POLICY_UNSPECIFIED", + Self::Gap => "REPORT_NULL_POLICY_GAP", + Self::Zero => "REPORT_NULL_POLICY_ZERO", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "REPORT_NULL_POLICY_UNSPECIFIED" => Some(Self::Unspecified), + "REPORT_NULL_POLICY_GAP" => Some(Self::Gap), + "REPORT_NULL_POLICY_ZERO" => Some(Self::Zero), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ReportCapability { + Unspecified = 0, + View = 1, + Filter = 2, + Drill = 3, + Export = 4, + Customize = 5, +} +impl ReportCapability { + /// 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_CAPABILITY_UNSPECIFIED", + Self::View => "REPORT_CAPABILITY_VIEW", + Self::Filter => "REPORT_CAPABILITY_FILTER", + Self::Drill => "REPORT_CAPABILITY_DRILL", + Self::Export => "REPORT_CAPABILITY_EXPORT", + Self::Customize => "REPORT_CAPABILITY_CUSTOMIZE", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "REPORT_CAPABILITY_UNSPECIFIED" => Some(Self::Unspecified), + "REPORT_CAPABILITY_VIEW" => Some(Self::View), + "REPORT_CAPABILITY_FILTER" => Some(Self::Filter), + "REPORT_CAPABILITY_DRILL" => Some(Self::Drill), + "REPORT_CAPABILITY_EXPORT" => Some(Self::Export), + "REPORT_CAPABILITY_CUSTOMIZE" => Some(Self::Customize), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ReportExecutionPurpose { + Unspecified = 0, + View = 1, + Preview = 2, + Export = 3, + Drill = 4, +} +impl ReportExecutionPurpose { + /// 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_EXECUTION_PURPOSE_UNSPECIFIED", + Self::View => "REPORT_EXECUTION_PURPOSE_VIEW", + Self::Preview => "REPORT_EXECUTION_PURPOSE_PREVIEW", + Self::Export => "REPORT_EXECUTION_PURPOSE_EXPORT", + Self::Drill => "REPORT_EXECUTION_PURPOSE_DRILL", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "REPORT_EXECUTION_PURPOSE_UNSPECIFIED" => Some(Self::Unspecified), + "REPORT_EXECUTION_PURPOSE_VIEW" => Some(Self::View), + "REPORT_EXECUTION_PURPOSE_PREVIEW" => Some(Self::Preview), + "REPORT_EXECUTION_PURPOSE_EXPORT" => Some(Self::Export), + "REPORT_EXECUTION_PURPOSE_DRILL" => Some(Self::Drill), + _ => None, + } + } +} /// Generated client implementations. pub mod analytics_service_client { #![allow( @@ -536,3 +1496,1085 @@ pub mod analytics_service_server { const NAME: &'static str = SERVICE_NAME; } } +/// Generated client implementations. +pub mod reporting_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct ReportingServiceClient { + inner: tonic::client::Grpc, + } + impl ReportingServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ReportingServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> ReportingServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + ReportingServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn list_assets( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.analytics.ReportingService/ListAssets", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("komp_ac.analytics.ReportingService", "ListAssets"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn get_asset( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.analytics.ReportingService/GetAsset", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("komp_ac.analytics.ReportingService", "GetAsset"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn save_draft( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.analytics.ReportingService/SaveDraft", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("komp_ac.analytics.ReportingService", "SaveDraft"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn publish( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.analytics.ReportingService/Publish", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("komp_ac.analytics.ReportingService", "Publish"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn list_versions( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.analytics.ReportingService/ListVersions", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("komp_ac.analytics.ReportingService", "ListVersions"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn restore_draft( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.analytics.ReportingService/RestoreDraft", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("komp_ac.analytics.ReportingService", "RestoreDraft"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn set_archived( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.analytics.ReportingService/SetArchived", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("komp_ac.analytics.ReportingService", "SetArchived"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn execute_dataset( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.analytics.ReportingService/ExecuteDataset", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.analytics.ReportingService", + "ExecuteDataset", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + pub async fn get_personal_views( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.analytics.ReportingService/GetPersonalViews", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.analytics.ReportingService", + "GetPersonalViews", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn save_personal_view( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.analytics.ReportingService/SavePersonalView", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.analytics.ReportingService", + "SavePersonalView", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn delete_personal_view( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.analytics.ReportingService/DeletePersonalView", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.analytics.ReportingService", + "DeletePersonalView", + ), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod reporting_service_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with ReportingServiceServer. + #[async_trait] + pub trait ReportingService: std::marker::Send + std::marker::Sync + 'static { + async fn list_assets( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_asset( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn save_draft( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn publish( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn list_versions( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn restore_draft( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn set_archived( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + /// Server streaming response type for the ExecuteDataset method. + type ExecuteDatasetStream: tonic::codegen::tokio_stream::Stream< + Item = std::result::Result, + > + + std::marker::Send + + 'static; + async fn execute_dataset( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_personal_views( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn save_personal_view( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn delete_personal_view( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + #[derive(Debug)] + pub struct ReportingServiceServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl ReportingServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for ReportingServiceServer + where + T: ReportingService, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/komp_ac.analytics.ReportingService/ListAssets" => { + #[allow(non_camel_case_types)] + struct ListAssetsSvc(pub Arc); + impl< + T: ReportingService, + > tonic::server::UnaryService + for ListAssetsSvc { + type Response = super::ListReportAssetsResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::list_assets(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ListAssetsSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.analytics.ReportingService/GetAsset" => { + #[allow(non_camel_case_types)] + struct GetAssetSvc(pub Arc); + impl< + T: ReportingService, + > tonic::server::UnaryService + for GetAssetSvc { + type Response = super::ReportAsset; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_asset(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetAssetSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.analytics.ReportingService/SaveDraft" => { + #[allow(non_camel_case_types)] + struct SaveDraftSvc(pub Arc); + impl< + T: ReportingService, + > tonic::server::UnaryService + for SaveDraftSvc { + type Response = super::ReportAsset; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::save_draft(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SaveDraftSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.analytics.ReportingService/Publish" => { + #[allow(non_camel_case_types)] + struct PublishSvc(pub Arc); + impl< + T: ReportingService, + > tonic::server::UnaryService + for PublishSvc { + type Response = super::ReportAsset; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::publish(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = PublishSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.analytics.ReportingService/ListVersions" => { + #[allow(non_camel_case_types)] + struct ListVersionsSvc(pub Arc); + impl< + T: ReportingService, + > tonic::server::UnaryService + for ListVersionsSvc { + type Response = super::ListReportVersionsResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::list_versions(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ListVersionsSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.analytics.ReportingService/RestoreDraft" => { + #[allow(non_camel_case_types)] + struct RestoreDraftSvc(pub Arc); + impl< + T: ReportingService, + > tonic::server::UnaryService + for RestoreDraftSvc { + type Response = super::ReportAsset; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::restore_draft(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = RestoreDraftSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.analytics.ReportingService/SetArchived" => { + #[allow(non_camel_case_types)] + struct SetArchivedSvc(pub Arc); + impl< + T: ReportingService, + > tonic::server::UnaryService + for SetArchivedSvc { + type Response = super::ReportAsset; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::set_archived(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SetArchivedSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.analytics.ReportingService/ExecuteDataset" => { + #[allow(non_camel_case_types)] + struct ExecuteDatasetSvc(pub Arc); + impl< + T: ReportingService, + > tonic::server::ServerStreamingService< + super::ExecuteReportDatasetRequest, + > for ExecuteDatasetSvc { + type Response = super::AnalyticsResultBatch; + type ResponseStream = T::ExecuteDatasetStream; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::execute_dataset(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ExecuteDatasetSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.server_streaming(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.analytics.ReportingService/GetPersonalViews" => { + #[allow(non_camel_case_types)] + struct GetPersonalViewsSvc(pub Arc); + impl< + T: ReportingService, + > tonic::server::UnaryService + for GetPersonalViewsSvc { + type Response = super::GetReportPersonalViewsResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_personal_views(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetPersonalViewsSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.analytics.ReportingService/SavePersonalView" => { + #[allow(non_camel_case_types)] + struct SavePersonalViewSvc(pub Arc); + impl< + T: ReportingService, + > tonic::server::UnaryService + for SavePersonalViewSvc { + type Response = super::ReportPersonalView; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::save_personal_view(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SavePersonalViewSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.analytics.ReportingService/DeletePersonalView" => { + #[allow(non_camel_case_types)] + struct DeletePersonalViewSvc(pub Arc); + impl< + T: ReportingService, + > tonic::server::UnaryService + for DeletePersonalViewSvc { + type Response = super::ReportMutationResult; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::DeleteReportPersonalViewRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::delete_personal_view( + &inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = DeletePersonalViewSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new( + tonic::body::Body::default(), + ); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for ReportingServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "komp_ac.analytics.ReportingService"; + impl tonic::server::NamedService for ReportingServiceServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/common/src/reporting.rs b/common/src/reporting.rs new file mode 100644 index 00000000..f3a2fb75 --- /dev/null +++ b/common/src/reporting.rs @@ -0,0 +1,550 @@ +use crate::proto::komp_ac::analytics::*; +use std::collections::{HashMap, HashSet}; + +pub const REPORT_SCHEMA_VERSION: u32 = 1; +pub const MAX_REPORT_BYTES: usize = 1024 * 1024; +pub const MAX_DATASET_ROWS: u32 = 10_000; + +pub fn enum_value>(value: i32, field: &str) -> Result { + if value == 0 { + return Err(format!("{field} is required")); + } + T::try_from(value).map_err(|_| format!("Invalid {field}: {value}")) +} + +pub fn asset_kind(definition: &ReportDefinition) -> Result { + match &definition.content { + Some(report_definition::Content::Dataset(_)) => Ok(ReportAssetKind::Dataset), + Some(report_definition::Content::Dashboard(_)) => Ok(ReportAssetKind::Dashboard), + None => Err("A report definition is required".into()), + } +} + +pub fn title(definition: &ReportDefinition) -> &str { + match &definition.content { + Some(report_definition::Content::Dataset(dataset)) => &dataset.title, + Some(report_definition::Content::Dashboard(dashboard)) => &dashboard.title, + None => "", + } +} + +pub fn description(definition: &ReportDefinition) -> &str { + match &definition.content { + Some(report_definition::Content::Dataset(dataset)) => &dataset.description, + Some(report_definition::Content::Dashboard(dashboard)) => &dashboard.description, + None => "", + } +} + +fn nonempty(value: &str, label: &str, limit: usize) -> Result<(), String> { + if value.trim().is_empty() || value.len() > limit || value.contains('\0') { + return Err(format!("{label} must contain between 1 and {limit} bytes")); + } + Ok(()) +} + +pub fn validate_parameter(parameter: &ReportParameter, allow_lookup: bool) -> Result<(), String> { + nonempty(¶meter.key, "Parameter key", 64)?; + if !parameter.key.bytes().enumerate().all(|(index, byte)| { + byte.is_ascii_alphabetic() || byte == b'_' || (index > 0 && byte.is_ascii_digit()) + }) { + return Err(format!( + "Parameter '{}' must be a SQL identifier", + parameter.key + )); + } + nonempty(¶meter.label, "Parameter label", 160)?; + let kind: ReportDataType = enum_value(parameter.data_type, "parameter data type")?; + let control: ReportFilterControl = enum_value(parameter.control, "filter control")?; + let default: ReportDefaultKind = enum_value(parameter.default_kind, "parameter default")?; + let valid_control = match control { + ReportFilterControl::Text => kind == ReportDataType::Text, + ReportFilterControl::Number => { + matches!(kind, ReportDataType::Integer | ReportDataType::Decimal) + } + ReportFilterControl::Date => kind == ReportDataType::Date, + ReportFilterControl::Timestamp => kind == ReportDataType::Timestamp, + ReportFilterControl::Checkbox => kind == ReportDataType::Boolean, + ReportFilterControl::Select | ReportFilterControl::Multiselect => true, + ReportFilterControl::Unspecified => false, + }; + if !valid_control || parameter.multiple != (control == ReportFilterControl::Multiselect) { + return Err(format!( + "Incompatible control for parameter '{}'", + parameter.key + )); + } + if parameter.default_values.len() > 500 + || (!parameter.multiple && parameter.default_values.len() > 1) + { + return Err(format!( + "Too many default values for parameter '{}'", + parameter.key + )); + } + if default != ReportDefaultKind::Literal + && (kind != ReportDataType::Date + || parameter.multiple + || !parameter.default_values.is_empty()) + { + return Err( + "Relative defaults require a single date parameter without literal defaults".into(), + ); + } + if parameter.choices.len() > 500 { + return Err("A filter can have at most 500 static choices".into()); + } + let mut choices = HashSet::new(); + for choice in ¶meter.choices { + nonempty(&choice.label, "Choice label", 160)?; + let value = choice.value.as_ref().ok_or("A choice value is required")?; + if !choices.insert(value) { + return Err(format!( + "Duplicate choices for parameter '{}'", + parameter.key + )); + } + } + if let Some(lookup) = ¶meter.lookup { + if !allow_lookup + || !parameter.choices.is_empty() + || !matches!( + control, + ReportFilterControl::Select | ReportFilterControl::Multiselect + ) + { + return Err( + "Lookup datasets belong to dashboard selection filters without static choices" + .into(), + ); + } + nonempty(&lookup.dataset_id, "Lookup dataset", 64)?; + nonempty(&lookup.value_field, "Lookup value field", 256)?; + nonempty(&lookup.label_field, "Lookup label field", 256)?; + } + if (!parameter.choices.is_empty() || parameter.lookup.is_some()) + && !matches!( + control, + ReportFilterControl::Select | ReportFilterControl::Multiselect + ) + { + return Err("Only selection filters can have choices".into()); + } + Ok(()) +} + +pub fn validate_definition(definition: &ReportDefinition) -> Result<(), String> { + if definition.schema_version != REPORT_SCHEMA_VERSION { + return Err(format!( + "Unsupported report schema version {}", + definition.schema_version + )); + } + if serde_json::to_vec(definition) + .map_err(|error| error.to_string())? + .len() + > MAX_REPORT_BYTES + { + return Err("Report definition exceeds 1 MiB".into()); + } + nonempty(title(definition), "Title", 160)?; + if description(definition).len() > 4000 { + return Err("Description exceeds 4000 bytes".into()); + } + match definition + .content + .as_ref() + .ok_or("A report definition is required")? + { + report_definition::Content::Dataset(dataset) => validate_dataset(dataset), + report_definition::Content::Dashboard(dashboard) => validate_dashboard(dashboard), + } +} + +fn validate_dataset(dataset: &ReportDatasetDefinition) -> Result<(), String> { + nonempty(&dataset.sql, "SQL", 64 * 1024)?; + if dataset.max_rows == 0 || dataset.max_rows > MAX_DATASET_ROWS { + return Err(format!( + "Dataset row limit must be between 1 and {MAX_DATASET_ROWS}" + )); + } + if dataset.parameters.len() > 32 || dataset.columns.is_empty() || dataset.columns.len() > 128 { + return Err("Datasets allow up to 32 parameters and between 1 and 128 columns".into()); + } + let mut keys = HashSet::new(); + for parameter in &dataset.parameters { + validate_parameter(parameter, false)?; + if !keys.insert(¶meter.key) { + return Err(format!("Duplicate parameter '{}'", parameter.key)); + } + } + keys.clear(); + for column in &dataset.columns { + nonempty(&column.key, "Column key", 256)?; + nonempty(&column.label, "Column label", 160)?; + if !keys.insert(&column.key) { + return Err(format!("Duplicate dataset column '{}'", column.key)); + } + let kind: ReportDataType = enum_value(column.data_type, "column data type")?; + let format: ReportNumberFormat = enum_value(column.number_format, "number format")?; + if column.fraction_digits.is_some_and(|digits| digits > 28) { + return Err("Fraction digits cannot exceed 28".into()); + } + if format != ReportNumberFormat::Number + && !matches!(kind, ReportDataType::Integer | ReportDataType::Decimal) + { + return Err("Number formatting requires a numeric column".into()); + } + if format == ReportNumberFormat::Currency { + if column.currency.is_empty() == column.currency_field.is_empty() { + return Err( + "Currency formatting needs either a currency code or a currency field".into(), + ); + } + if !column.currency.is_empty() + && (column.currency.len() != 3 + || !column + .currency + .bytes() + .all(|byte| byte.is_ascii_uppercase())) + { + return Err("Currency codes must have three uppercase letters".into()); + } + } + } + for column in &dataset.columns { + if !column.currency_field.is_empty() + && !dataset.columns.iter().any(|other| { + other.key == column.currency_field && other.data_type == ReportDataType::Text as i32 + }) + { + return Err(format!("Invalid currency field for '{}'", column.key)); + } + } + Ok(()) +} + +fn validate_dashboard(dashboard: &ReportDashboardDefinition) -> Result<(), String> { + let referenced = referenced_datasets(dashboard); + let mut dataset_versions = HashSet::new(); + for reference in &dashboard.dataset_versions { + if !referenced.contains(reference.dataset_id.as_str()) || !dataset_versions.insert(&reference.dataset_id) { + return Err("Dataset version references must be unique and used by the dashboard".into()); + } + } + if dashboard.filters.len() > 32 + || dashboard.panels.is_empty() + || dashboard.panels.len() > 32 + || dashboard.grants.len() > 200 + { + return Err( + "Dashboards allow up to 32 filters, between 1 and 32 panels, and up to 200 grants" + .into(), + ); + } + if dashboard.refresh_seconds != 0 && !(30..=86400).contains(&dashboard.refresh_seconds) { + return Err("Refresh interval must be disabled or between 30 and 86400 seconds".into()); + } + let mut filters = HashSet::new(); + let mut targets = HashSet::new(); + for filter in &dashboard.filters { + let parameter = filter + .parameter + .as_ref() + .ok_or("A dashboard filter needs a parameter")?; + validate_parameter(parameter, true)?; + if !filters.insert(¶meter.key) || filter.targets.is_empty() || filter.targets.len() > 32 + { + return Err("Dashboard filters need unique keys and between 1 and 32 targets".into()); + } + for target in &filter.targets { + if !targets.insert((&target.dataset_id, &target.parameter_key)) { + return Err("A dataset parameter cannot be controlled by multiple filters".into()); + } + } + } + let mut panels = HashSet::new(); + for panel in &dashboard.panels { + nonempty(&panel.id, "Panel ID", 64)?; + nonempty(&panel.title, "Panel title", 160)?; + nonempty(&panel.dataset_id, "Panel dataset", 64)?; + if !panels.insert(&panel.id) { + return Err("Panel IDs must be unique".into()); + } + let _: ReportPanelKind = enum_value(panel.kind, "panel kind")?; + let _: ReportOrientation = enum_value(panel.orientation, "orientation")?; + let sort: ReportSortOrder = enum_value(panel.sort_order, "sort order")?; + let _: ReportNullPolicy = enum_value(panel.null_policy, "null policy")?; + if !(1..=12).contains(&panel.width) || !(160..=1200).contains(&panel.height) { + return Err("Panel width must be 1–12 and height 160–1200".into()); + } + if sort != ReportSortOrder::Source && panel.sort_field.is_empty() { + return Err("A sorted panel needs a sort field".into()); + } + if panel.axis_min.is_some_and(|number| !number.is_finite()) + || panel.axis_max.is_some_and(|number| !number.is_finite()) + || matches!((panel.axis_min, panel.axis_max), (Some(min), Some(max)) if min >= max) + { + return Err("Invalid chart axis bounds".into()); + } + if panel.colors.len() > 32 + || panel.colors.iter().any(|color| { + !matches!(color.len(), 4 | 7) + || !color.starts_with('#') + || !color[1..].bytes().all(|byte| byte.is_ascii_hexdigit()) + }) + { + return Err("Chart colors must be hexadecimal CSS colors".into()); + } + if panel.actions.len() > 8 { + return Err("A panel can have at most eight actions".into()); + } + for action in &panel.actions { + nonempty(&action.label, "Action label", 160)?; + match action + .target + .as_ref() + .ok_or("An action target is required")? + { + report_panel_action::Target::Filter(action) => { + if action.bindings.is_empty() + || action.bindings.len() > 32 + || action + .bindings + .iter() + .any(|binding| !filters.contains(&binding.filter_key)) + { + return Err("Filter actions must target defined dashboard filters".into()); + } + } + report_panel_action::Target::Record(action) => { + nonempty(&action.table_name, "Record table", 256)?; + nonempty(&action.id_field, "Record ID field", 256)?; + } + report_panel_action::Target::Dashboard(action) => { + nonempty(&action.dashboard_id, "Target dashboard", 64)?; + if action.bindings.len() > 32 { + return Err("A dashboard action can supply at most 32 filters".into()); + } + } + } + } + } + let mut subjects = HashSet::new(); + for grant in &dashboard.grants { + let subject = grant + .subject + .as_ref() + .ok_or("A grant subject is required")?; + let value = match subject { + report_grant::Subject::Role(role) => role, + report_grant::Subject::UserId(user) => user, + }; + nonempty(value, "Grant subject", 128)?; + if !subjects.insert(subject) { + return Err("Duplicate report grant subject".into()); + } + let mut capabilities = HashSet::new(); + for capability in &grant.capabilities { + capabilities.insert(enum_value::(*capability, "capability")?); + } + if !capabilities.contains(&ReportCapability::View) + || capabilities.len() != grant.capabilities.len() + { + return Err("Report grants need View and unique capabilities".into()); + } + } + Ok(()) +} + +pub fn referenced_datasets(dashboard: &ReportDashboardDefinition) -> HashSet<&str> { + dashboard + .panels + .iter() + .map(|panel| panel.dataset_id.as_str()) + .chain(dashboard.filters.iter().filter_map(|filter| { + filter + .parameter + .as_ref()? + .lookup + .as_ref() + .map(|lookup| lookup.dataset_id.as_str()) + })) + .collect() +} + +pub fn validate_dashboard_datasets( + dashboard: &ReportDashboardDefinition, + datasets: &[ReportDatasetSnapshot], +) -> Result<(), String> { + let datasets = datasets + .iter() + .map(|snapshot| { + snapshot + .definition + .as_ref() + .map(|definition| (snapshot.dataset_id.as_str(), definition)) + .ok_or("Missing dataset snapshot".to_string()) + }) + .collect::, _>>()?; + for id in referenced_datasets(dashboard) { + if !datasets.contains_key(id) { + return Err(format!("Dataset '{id}' is unavailable")); + } + } + for filter in &dashboard.filters { + let parameter = filter + .parameter + .as_ref() + .ok_or("Missing filter parameter")?; + for target in &filter.targets { + let target_parameter = datasets + .get(target.dataset_id.as_str()) + .and_then(|dataset| { + dataset + .parameters + .iter() + .find(|item| item.key == target.parameter_key) + }) + .ok_or_else(|| { + format!( + "Unknown filter target '{}.{}'", + target.dataset_id, target.parameter_key + ) + })?; + if parameter.data_type != target_parameter.data_type + || parameter.multiple != target_parameter.multiple + { + return Err(format!( + "Filter '{}' has an incompatible dataset parameter", + parameter.key + )); + } + } + if let Some(lookup) = ¶meter.lookup { + let dataset = datasets + .get(lookup.dataset_id.as_str()) + .ok_or("Unknown lookup dataset")?; + if !dataset.columns.iter().any(|column| { + column.key == lookup.value_field && column.data_type == parameter.data_type + }) || !dataset + .columns + .iter() + .any(|column| column.key == lookup.label_field) + { + return Err(format!("Invalid lookup fields for '{}'", parameter.key)); + } + if filter + .targets + .iter() + .any(|target| target.dataset_id == lookup.dataset_id) + { + return Err("A lookup filter cannot filter its own choice dataset".into()); + } + } + } + for panel in &dashboard.panels { + let dataset = datasets + .get(panel.dataset_id.as_str()) + .ok_or("Unknown panel dataset")?; + let columns: HashMap<_, _> = dataset + .columns + .iter() + .map(|column| (column.key.as_str(), column)) + .collect(); + let kind: ReportPanelKind = enum_value(panel.kind, "panel kind")?; + if panel.y_fields.iter().collect::>().len() != panel.y_fields.len() + || panel.table_fields.iter().collect::>().len() != panel.table_fields.len() + { + return Err("Panel field selections must be unique".into()); + } + let field = |key: &str| { + columns + .get(key) + .copied() + .ok_or_else(|| format!("Panel '{}' refers to unknown field '{key}'", panel.title)) + }; + for key in [ + &panel.x_field, + &panel.series_field, + &panel.size_field, + &panel.sort_field, + ] + .into_iter() + .filter(|key| !key.is_empty()) + .chain(panel.y_fields.iter()) + .chain(panel.table_fields.iter()) + { + field(key)?; + } + if kind != ReportPanelKind::Table { + if panel.y_fields.is_empty() || panel.y_fields.len() > 16 { + return Err("Charts need between 1 and 16 measures".into()); + } + for key in &panel.y_fields { + let kind: ReportDataType = enum_value(field(key)?.data_type, "measure type")?; + if !matches!(kind, ReportDataType::Integer | ReportDataType::Decimal) { + return Err("Chart measures must be numeric".into()); + } + } + if !matches!(kind, ReportPanelKind::Kpi | ReportPanelKind::Gauge) { + field(&panel.x_field)?; + } + if matches!( + kind, + ReportPanelKind::Pie + | ReportPanelKind::Donut + | ReportPanelKind::Treemap + | ReportPanelKind::Funnel + | ReportPanelKind::Gauge + | ReportPanelKind::Waterfall + | ReportPanelKind::Heatmap + | ReportPanelKind::Kpi + ) && panel.y_fields.len() != 1 + { + return Err("This panel kind requires exactly one measure".into()); + } + if kind == ReportPanelKind::Heatmap { + field(&panel.series_field)?; + } + for key in [ + (kind == ReportPanelKind::Scatter).then_some(panel.x_field.as_str()), + (!panel.size_field.is_empty()).then_some(panel.size_field.as_str()), + ].into_iter().flatten() { + let data_type: ReportDataType = enum_value(field(key)?.data_type, "numeric field type")?; + if !matches!(data_type, ReportDataType::Integer | ReportDataType::Decimal) { + return Err("Scatter coordinates and size fields must be numeric".into()); + } + } + } + for action in &panel.actions { + match action.target.as_ref().ok_or("Missing action target")? { + report_panel_action::Target::Filter(action) => { + for binding in &action.bindings { + let source = field(&binding.column_key)?; + let target = dashboard + .filters + .iter() + .filter_map(|filter| filter.parameter.as_ref()) + .find(|parameter| parameter.key == binding.filter_key) + .ok_or("Unknown action filter")?; + if source.data_type != target.data_type { + return Err("Action field and filter types must match".into()); + } + } + } + report_panel_action::Target::Record(action) => { + if field(&action.id_field)?.data_type != ReportDataType::Integer as i32 { + return Err("Record navigation requires an integer ID field".into()); + } + } + report_panel_action::Target::Dashboard(action) => { + for binding in &action.bindings { + field(&binding.column_key)?; + } + } + } + } + } + Ok(()) +} diff --git a/komp-app/src/analytics.rs b/komp-app/src/analytics.rs new file mode 100644 index 00000000..44749357 --- /dev/null +++ b/komp-app/src/analytics.rs @@ -0,0 +1,249 @@ +use anyhow::{Result, bail, ensure}; +use common::proto::komp_ac::analytics::{ + AnalyticsResultBatch, AnalyticsResultColumn, AnalyticsValue, analytics_value::Kind, +}; +use serde::Serialize; + +const MAX_ROWS: usize = 10_000; +const MAX_RESULT_BYTES: usize = 32 * 1024 * 1024; + +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(tag = "kind", content = "value", rename_all = "snake_case")] +pub enum Cell { + Null, + Text(String), + Integer(String), + Unsigned(String), + Float(f64), + Boolean(bool), + Bytes(String), +} + +impl TryFrom for Cell { + type Error = anyhow::Error; + + fn try_from(value: AnalyticsValue) -> Result { + Ok(match value.kind { + Some(Kind::NullValue(0)) => Self::Null, + Some(Kind::StringValue(value)) => Self::Text(value), + Some(Kind::Int64Value(value)) => Self::Integer(value.to_string()), + Some(Kind::Uint64Value(value)) => Self::Unsigned(value.to_string()), + Some(Kind::DoubleValue(value)) if value.is_finite() => Self::Float(value), + Some(Kind::BoolValue(value)) => Self::Boolean(value), + Some(Kind::BytesValue(value)) => { + Self::Bytes(value.iter().map(|byte| format!("{byte:02x}")).collect()) + } + _ => bail!("Analytics returned an invalid or non-finite cell"), + }) + } +} + +impl Cell { + fn size(&self) -> usize { + match self { + Self::Text(value) + | Self::Integer(value) + | Self::Unsigned(value) + | Self::Bytes(value) => value.len().saturating_add(32), + _ => 32, + } + } +} + +#[derive(Debug, Default, Serialize)] +pub struct QueryResult { + pub columns: Vec, + pub rows: Vec>, + pub row_count: u64, + pub elapsed_ms: u64, + pub truncated: bool, +} + +#[derive(Default)] +struct Collector { + result: QueryResult, + bytes: usize, + finished: bool, +} + +impl Collector { + fn push(&mut self, batch: AnalyticsResultBatch) -> Result<()> { + ensure!(!self.finished, "Analytics sent data after completion"); + if !batch.columns.is_empty() { + if self.result.columns.is_empty() { + self.result.columns = batch.columns; + } else { + ensure!( + self.result.columns == batch.columns, + "Analytics changed its result schema" + ); + } + } + ensure!( + self.result.rows.len().saturating_add(batch.rows.len()) <= MAX_ROWS, + "Analytics result exceeds the client row limit" + ); + for row in batch.rows { + ensure!( + row.values.len() == self.result.columns.len(), + "Analytics row does not match its schema" + ); + let row: Vec<_> = row + .values + .into_iter() + .map(Cell::try_from) + .collect::>()?; + self.bytes = self + .bytes + .saturating_add(row.iter().map(Cell::size).sum::()); + ensure!( + self.bytes <= MAX_RESULT_BYTES, + "Analytics result exceeds 32 MiB; narrow the query or filters" + ); + self.result.rows.push(row); + } + if batch.is_final { + ensure!( + batch.row_count == self.result.rows.len() as u64, + "Analytics result is incomplete" + ); + self.finished = true; + self.result.row_count = batch.row_count; + self.result.elapsed_ms = batch.elapsed_ms; + self.result.truncated = batch.truncated; + } + Ok(()) + } + + fn finish(self) -> Result { + ensure!(self.finished, "Analytics stream ended before completion"); + Ok(self.result) + } +} + +pub async fn collect_result( + mut stream: tonic::Streaming, +) -> Result { + let mut collector = Collector::default(); + while let Some(batch) = stream.message().await? { + collector.push(batch)?; + } + collector.finish() +} + +#[cfg(test)] +mod tests { + use super::*; + use common::proto::komp_ac::analytics::AnalyticsResultRow; + + #[test] + fn ipc_preserves_exact_values_and_distinguishes_null_from_empty_text() { + assert_eq!( + Cell::try_from(AnalyticsValue { + kind: Some(Kind::Int64Value(i64::MAX)) + }) + .unwrap(), + Cell::Integer("9223372036854775807".into()) + ); + assert_eq!( + Cell::try_from(AnalyticsValue { + kind: Some(Kind::Uint64Value(u64::MAX)) + }) + .unwrap(), + Cell::Unsigned("18446744073709551615".into()) + ); + assert_eq!( + Cell::try_from(AnalyticsValue { + kind: Some(Kind::StringValue("999999999999999999.99".into())) + }) + .unwrap(), + Cell::Text("999999999999999999.99".into()) + ); + assert_ne!( + Cell::try_from(AnalyticsValue { + kind: Some(Kind::NullValue(0)) + }) + .unwrap(), + Cell::try_from(AnalyticsValue { + kind: Some(Kind::StringValue(String::new())) + }) + .unwrap() + ); + assert!(Cell::try_from(AnalyticsValue { kind: None }).is_err()); + assert!( + Cell::try_from(AnalyticsValue { + kind: Some(Kind::DoubleValue(f64::NAN)) + }) + .is_err() + ); + } + + fn data() -> AnalyticsResultBatch { + AnalyticsResultBatch { + columns: vec![AnalyticsResultColumn { + name: "amount".into(), + data_type: "Decimal128(20, 2)".into(), + }], + rows: vec![AnalyticsResultRow { + values: vec![AnalyticsValue { + kind: Some(Kind::StringValue("123.45".into())), + }], + }], + ..Default::default() + } + } + + #[test] + fn interrupted_stream_is_never_returned_as_a_successful_partial_report() { + let mut collector = Collector::default(); + collector.push(data()).unwrap(); + assert!(collector.finish().is_err()); + let mut collector = Collector::default(); + collector.push(data()).unwrap(); + assert!( + collector + .push(AnalyticsResultBatch { + is_final: true, + row_count: 2, + ..Default::default() + }) + .is_err() + ); + } + + #[test] + fn completion_preserves_schema_and_truncation() { + let mut collector = Collector::default(); + collector.push(data()).unwrap(); + collector + .push(AnalyticsResultBatch { + is_final: true, + row_count: 1, + truncated: true, + elapsed_ms: 7, + ..Default::default() + }) + .unwrap(); + let result = collector.finish().unwrap(); + assert_eq!(result.columns[0].data_type, "Decimal128(20, 2)"); + assert_eq!(result.row_count, 1); + assert_eq!(result.elapsed_ms, 7); + assert!(result.truncated); + } + + #[test] + fn mismatched_schema_and_post_completion_batches_are_rejected() { + let mut collector = Collector::default(); + let mut invalid = data(); + invalid.rows[0].values.clear(); + assert!(collector.push(invalid).is_err()); + let mut collector = Collector::default(); + collector + .push(AnalyticsResultBatch { + is_final: true, + ..Default::default() + }) + .unwrap(); + assert!(collector.push(data()).is_err()); + } +} diff --git a/komp-app/src/grpc.rs b/komp-app/src/grpc.rs index 9d21b26f..f9eaf0b8 100644 --- a/komp-app/src/grpc.rs +++ b/komp-app/src/grpc.rs @@ -1,4 +1,5 @@ use crate::search::SearchGrpc; +mod reporting; use anyhow::{Context, Result, anyhow}; use crate::transport::{ DEFAULT_GRPC_ENDPOINT, authenticated_request as request_with_auth_token, connect_channel, diff --git a/komp-app/src/grpc/reporting.rs b/komp-app/src/grpc/reporting.rs new file mode 100644 index 00000000..bf030f81 --- /dev/null +++ b/komp-app/src/grpc/reporting.rs @@ -0,0 +1,134 @@ +use super::GrpcClient; +use anyhow::{Context, Result}; +use common::proto::komp_ac::analytics::{reporting_service_client::ReportingServiceClient, *}; + +impl GrpcClient { + pub async fn list_report_assets( + &mut self, + request: ListReportAssetsRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let response = ReportingServiceClient::new(self.channel.clone()) + .list_assets(request) + .await + .context("gRPC ReportingService list_assets call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_report_asset( + &mut self, + request: GetReportAssetRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let response = ReportingServiceClient::new(self.channel.clone()) + .get_asset(request) + .await + .context("gRPC ReportingService get_asset call failed")?; + Ok(response.into_inner()) + } + + pub async fn save_report_draft( + &mut self, + request: SaveReportDraftRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let response = ReportingServiceClient::new(self.channel.clone()) + .save_draft(request) + .await + .context("gRPC ReportingService save_draft call failed")?; + Ok(response.into_inner()) + } + + pub async fn publish_report(&mut self, request: PublishReportRequest) -> Result { + let request = self.authenticated_request(request)?; + let response = ReportingServiceClient::new(self.channel.clone()) + .publish(request) + .await + .context("gRPC ReportingService publish call failed")?; + Ok(response.into_inner()) + } + + pub async fn list_report_versions( + &mut self, + request: ReportAssetRef, + ) -> Result { + let request = self.authenticated_request(request)?; + let response = ReportingServiceClient::new(self.channel.clone()) + .list_versions(request) + .await + .context("gRPC ReportingService list_versions call failed")?; + Ok(response.into_inner()) + } + + pub async fn restore_report_draft( + &mut self, + request: RestoreReportDraftRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let response = ReportingServiceClient::new(self.channel.clone()) + .restore_draft(request) + .await + .context("gRPC ReportingService restore_draft call failed")?; + Ok(response.into_inner()) + } + + pub async fn set_report_archived( + &mut self, + request: SetReportArchivedRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let response = ReportingServiceClient::new(self.channel.clone()) + .set_archived(request) + .await + .context("gRPC ReportingService set_archived call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_report_personal_views( + &mut self, + request: ReportAssetRef, + ) -> Result { + let request = self.authenticated_request(request)?; + let response = ReportingServiceClient::new(self.channel.clone()) + .get_personal_views(request) + .await + .context("gRPC ReportingService get_personal_views call failed")?; + Ok(response.into_inner()) + } + + pub async fn save_report_personal_view( + &mut self, + request: SaveReportPersonalViewRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let response = ReportingServiceClient::new(self.channel.clone()) + .save_personal_view(request) + .await + .context("gRPC ReportingService save_personal_view call failed")?; + Ok(response.into_inner()) + } + + pub async fn delete_report_personal_view( + &mut self, + request: DeleteReportPersonalViewRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let response = ReportingServiceClient::new(self.channel.clone()) + .delete_personal_view(request) + .await + .context("gRPC ReportingService delete_personal_view call failed")?; + Ok(response.into_inner()) + } + + pub async fn execute_report_dataset( + &mut self, + request: ExecuteReportDatasetRequest, + ) -> Result> { + let request = self.authenticated_request(request)?; + let response = ReportingServiceClient::new(self.channel.clone()) + .execute_dataset(request) + .await + .context("gRPC ReportingService execute_dataset call failed")?; + Ok(response.into_inner()) + } +} diff --git a/komp-app/src/lib.rs b/komp-app/src/lib.rs index 247f7b24..34273de5 100644 --- a/komp-app/src/lib.rs +++ b/komp-app/src/lib.rs @@ -5,6 +5,7 @@ //! owns client behavior that must not drift between them. pub mod auth; +pub mod analytics; pub mod csv; pub mod grpc; pub mod import_export;