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 d8e28487..b4f761dd 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ 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;