From b7a952d22e5d8ee807cbdd6f89bd9156008e63f3 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Fri, 6 Sep 2024 14:12:59 +0200 Subject: [PATCH] wip --- bundle/config/generate/dashboard.go | 19 + bundle/config/mutator/apply_presets.go | 5 + cmd/bundle/generate.go | 1 + cmd/bundle/generate/dashboard.go | 281 +++++++ cmd/bundle/generate/dashboard_test.go | 1 + .../nyc_taxi_trip_analysis.lvdash.json | 710 ++++++++++++++++++ tmp/dashboard-generate/databricks.yml | 16 + .../resources/nyc_taxi_trip_analysis.yml | 23 + 8 files changed, 1056 insertions(+) create mode 100644 bundle/config/generate/dashboard.go create mode 100644 cmd/bundle/generate/dashboard.go create mode 100644 cmd/bundle/generate/dashboard_test.go create mode 100644 tmp/dashboard-generate/dashboards/nyc_taxi_trip_analysis.lvdash.json create mode 100644 tmp/dashboard-generate/databricks.yml create mode 100644 tmp/dashboard-generate/resources/nyc_taxi_trip_analysis.yml diff --git a/bundle/config/generate/dashboard.go b/bundle/config/generate/dashboard.go new file mode 100644 index 0000000000..4f2c012db2 --- /dev/null +++ b/bundle/config/generate/dashboard.go @@ -0,0 +1,19 @@ +package generate + +import ( + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/databricks-sdk-go/service/dashboards" +) + +func ConvertDashboardToValue(dashboard *dashboards.Dashboard, filePath string) (dyn.Value, error) { + // The majority of fields of the dashboard struct are read-only. + // We copy the relevant fields manually. + dv := map[string]dyn.Value{ + "display_name": dyn.NewValue(dashboard.DisplayName, []dyn.Location{{Line: 1}}), + "parent_path": dyn.NewValue("${workspace.file_path}", []dyn.Location{{Line: 2}}), + "warehouse_id": dyn.NewValue(dashboard.WarehouseId, []dyn.Location{{Line: 3}}), + "definition_path": dyn.NewValue(filePath, []dyn.Location{{Line: 4}}), + } + + return dyn.V(dv), nil +} diff --git a/bundle/config/mutator/apply_presets.go b/bundle/config/mutator/apply_presets.go index 28d015c109..0a12174dc1 100644 --- a/bundle/config/mutator/apply_presets.go +++ b/bundle/config/mutator/apply_presets.go @@ -160,6 +160,11 @@ func (m *applyPresets) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnos // the Databricks UI and via the SQL API. } + // Dashboards: Prefix + for i := range r.Dashboards { + r.Dashboards[i].DisplayName = prefix + r.Dashboards[i].DisplayName + } + return nil } diff --git a/cmd/bundle/generate.go b/cmd/bundle/generate.go index 1e3d56e430..7dea19ff9d 100644 --- a/cmd/bundle/generate.go +++ b/cmd/bundle/generate.go @@ -16,6 +16,7 @@ func newGenerateCommand() *cobra.Command { cmd.AddCommand(generate.NewGenerateJobCommand()) cmd.AddCommand(generate.NewGeneratePipelineCommand()) + cmd.AddCommand(generate.NewGenerateDashboardCommand()) cmd.PersistentFlags().StringVar(&key, "key", "", `resource key to use for the generated configuration`) return cmd } diff --git a/cmd/bundle/generate/dashboard.go b/cmd/bundle/generate/dashboard.go new file mode 100644 index 0000000000..d0726ba632 --- /dev/null +++ b/cmd/bundle/generate/dashboard.go @@ -0,0 +1,281 @@ +package generate + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config/generate" + "github.com/databricks/cli/bundle/deploy/terraform" + "github.com/databricks/cli/bundle/phases" + "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/cli/libs/dyn/yamlsaver" + "github.com/databricks/cli/libs/textutil" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/service/dashboards" + "github.com/databricks/databricks-sdk-go/service/workspace" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +type dashboard struct { + resourceDir string + dashboardDir string + force bool + + // Relative path from the resource directory to the dashboard directory. + relativeDir string + + existingDashboardPath string + existingDashboardId string + watch string + + key string +} + +func (d *dashboard) resolveDashboardID(ctx context.Context, w *databricks.WorkspaceClient) diag.Diagnostics { + if d.existingDashboardPath == "" { + return nil + } + + obj, err := w.Workspace.GetStatusByPath(ctx, d.existingDashboardPath) + if err != nil { + return diag.FromErr(err) + } + + if obj.ObjectType != workspace.ObjectTypeDashboard { + found := strings.ToLower(obj.ObjectType.String()) + return diag.Diagnostics{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("expected a dashboard, found a %s", found), + Locations: []dyn.Location{{ + File: d.existingDashboardPath, + }}, + }, + } + } + + if obj.ResourceId == "" { + return diag.Diagnostics{ + { + Severity: diag.Error, + Summary: "expected resource ID to be set", + Locations: []dyn.Location{{ + File: d.existingDashboardPath, + }}, + }, + } + } + + d.existingDashboardId = obj.ResourceId + return nil +} + +func (d *dashboard) saveConfiguration(ctx context.Context, dashboard *dashboards.Dashboard) error { + // TODO: add flag + key := d.key + if key == "" { + key = textutil.NormalizeString(dashboard.DisplayName) + } + + dashboardFilePath := path.Join(d.relativeDir, fmt.Sprintf("%s.lvdash.json", key)) + v, err := generate.ConvertDashboardToValue(dashboard, dashboardFilePath) + if err != nil { + return err + } + + result := map[string]dyn.Value{ + "resources": dyn.V(map[string]dyn.Value{ + "dashboards": dyn.V(map[string]dyn.Value{ + key: v, + }), + }), + } + + // Make sure the output directory exists. + if err := os.MkdirAll(d.resourceDir, 0755); err != nil { + return err + } + + filename := filepath.Join(d.resourceDir, fmt.Sprintf("%s.yml", key)) + saver := yamlsaver.NewSaverWithStyle(map[string]yaml.Style{ + "display_name": yaml.DoubleQuotedStyle, + }) + err = saver.SaveAsYAML(result, filename, false) + if err != nil { + return err + } + + return nil +} + +func (d *dashboard) remarshal(data []byte) ([]byte, error) { + var tmp any + var err error + err = json.Unmarshal(data, &tmp) + if err != nil { + return nil, err + } + out, err := json.MarshalIndent(tmp, "", " ") + if err != nil { + return nil, err + } + return out, nil +} + +func (d *dashboard) saveSerializedDashboard(ctx context.Context, dashboard *dashboards.Dashboard, dst string) error { + // Unmarshal and remarshal the serialized dashboard to ensure it is formatted correctly. + // The result will have alphabetically sorted keys and be indented. + data, err := d.remarshal([]byte(dashboard.SerializedDashboard)) + if err != nil { + return err + } + + // Make sure the output directory exists. + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return err + } + + return os.WriteFile(dst, data, 0644) +} + +func (d *dashboard) watchForChanges(ctx context.Context, b *bundle.Bundle) error { + diags := bundle.Apply(ctx, b, bundle.Seq( + phases.Initialize(), + terraform.Interpolate(), + terraform.Write(), + terraform.StatePull(), + terraform.Load(terraform.ErrorOnEmptyState), + )) + if err := diags.Error(); err != nil { + return err + } + + dash, ok := b.Config.Resources.Dashboards[d.watch] + if !ok { + return fmt.Errorf("dashboard %s not found", d.watch) + } + + // fmt.Println(dash.DefinitionPath) + + w := b.WorkspaceClient() + etag := "" + + cwd, err := os.Getwd() + if err != nil { + return err + } + + relPath, err := filepath.Rel(cwd, dash.DefinitionPath) + if err != nil { + return err + } + + for { + dashboard, err := w.Lakeview.GetByDashboardId(ctx, dash.ID) + if err != nil { + return err + } + + // fmt.Println(dashboard.Path) + // fmt.Println(dashboard.Etag) + // fmt.Println(dashboard.UpdateTime) + + // obj, err := w.Workspace.GetStatusByPath(ctx, "/Users/pieter.noordhuis@databricks.com/.bundle/dashboard-eng-work-generate/dev/files/[dev pieter_noordhuis] NYC Taxi Trip Analysis.lvdash.json") + // if err != nil { + // return err + // } + + // fmt.Println(obj.ModifiedAt) + + if etag != dashboard.Etag { + fmt.Printf("[%s]: Updating dashboard at %s\n", dashboard.UpdateTime, relPath) + d.saveSerializedDashboard(ctx, dashboard, dash.DefinitionPath) + } + + etag = dashboard.Etag + time.Sleep(1 * time.Second) + } +} + +func (d *dashboard) RunE(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + b, diags := root.MustConfigureBundle(cmd) + if err := diags.Error(); err != nil { + return diags.Error() + } + + // Make sure we know how the dashboard path is relative to the resource path. + rel, err := filepath.Rel(d.resourceDir, d.dashboardDir) + if err != nil { + return err + } + + d.relativeDir = filepath.ToSlash(rel) + + w := b.WorkspaceClient() + + if d.watch != "" { + return d.watchForChanges(ctx, b) + } + + // Lookup the dashboard ID if the path is given + diags = d.resolveDashboardID(ctx, w) + if diags.HasError() { + return diags.Error() + } + + dashboard, err := w.Lakeview.GetByDashboardId(ctx, d.existingDashboardId) + if err != nil { + return err + } + + d.saveConfiguration(ctx, dashboard) + + // TODO: add flag + key := d.key + if key == "" { + key = textutil.NormalizeString(dashboard.DisplayName) + } + + filename := filepath.Join(d.dashboardDir, fmt.Sprintf("%s.lvdash.json", key)) + d.saveSerializedDashboard(ctx, dashboard, filename) + return nil +} + +func NewGenerateDashboardCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "dashboard", + Short: "Generate configuration for a dashboard", + } + + d := &dashboard{} + + cmd.Flags().StringVarP(&d.resourceDir, "resource-dir", "d", "./resources", `directory to write the configuration to`) + cmd.Flags().StringVarP(&d.dashboardDir, "dashboard-dir", "s", "./dashboards", `directory to write the dashboard representation to`) + cmd.Flags().BoolVarP(&d.force, "force", "f", false, `force overwrite existing files in the output directory`) + + // Specify dashboard by workspace path + + cmd.Flags().StringVar(&d.existingDashboardPath, "existing-dashboard-path", "", `workspace path of the dashboard to generate configuration for`) + cmd.Flags().StringVar(&d.existingDashboardId, "existing-dashboard-id", "", `ID of the dashboard to generate configuration for`) + cmd.Flags().StringVar(&d.watch, "watch-resource", "", `resource key of dashboard to watch for changes`) + + cmd.MarkFlagsOneRequired( + "existing-dashboard-path", + "existing-dashboard-id", + "watch-resource", + ) + + cmd.RunE = d.RunE + return cmd +} diff --git a/cmd/bundle/generate/dashboard_test.go b/cmd/bundle/generate/dashboard_test.go new file mode 100644 index 0000000000..eb8347795a --- /dev/null +++ b/cmd/bundle/generate/dashboard_test.go @@ -0,0 +1 @@ +package generate diff --git a/tmp/dashboard-generate/dashboards/nyc_taxi_trip_analysis.lvdash.json b/tmp/dashboard-generate/dashboards/nyc_taxi_trip_analysis.lvdash.json new file mode 100644 index 0000000000..162535efd3 --- /dev/null +++ b/tmp/dashboard-generate/dashboards/nyc_taxi_trip_analysis.lvdash.json @@ -0,0 +1,710 @@ +{ + "datasets": [ + { + "displayName": "route revenue", + "name": "fdefd57c", + "query": "SELECT\n T.pickup_zip,\n T.dropoff_zip,\n T.route as `Route`,\n T.frequency as `Number Trips`,\n T.total_fare as `Total Revenue`\nFROM\n (\n SELECT\n pickup_zip,\n dropoff_zip,\n concat(pickup_zip, '-', dropoff_zip) AS route,\n count(*) as frequency,\n SUM(fare_amount) as total_fare\n FROM\n `samples`.`nyctaxi`.`trips`\n GROUP BY\n 1,2,3\n ) T\nORDER BY\n 1 ASC" + }, + { + "displayName": "trips", + "name": "ecfcdc7c", + "query": "SELECT\n T.tpep_pickup_datetime,\n T.tpep_dropoff_datetime,\n T.fare_amount,\n T.pickup_zip,\n T.dropoff_zip,\n T.trip_distance,\n T.weekday,\n CASE\n WHEN T.weekday = 1 THEN 'Sunday'\n WHEN T.weekday = 2 THEN 'Monday'\n WHEN T.weekday = 3 THEN 'Tuesday'\n WHEN T.weekday = 4 THEN 'Wednesday'\n WHEN T.weekday = 5 THEN 'Thursday'\n WHEN T.weekday = 6 THEN 'Friday'\n WHEN T.weekday = 7 THEN 'Saturday'\n ELSE 'N/A'\n END AS day_of_week, \n T.fare_amount, \n T.trip_distance\nFROM\n (\n SELECT\n dayofweek(tpep_pickup_datetime) as weekday,\n *\n FROM\n `samples`.`nyctaxi`.`trips`\n WHERE\n trip_distance \u003e 0\n AND trip_distance \u003c 10\n AND fare_amount \u003e 0\n AND fare_amount \u003c 50\n ) T\nORDER BY\n T.weekday " + } + ], + "pages": [ + { + "displayName": "New Page", + "layout": [ + { + "position": { + "height": 1, + "width": 2, + "x": 0, + "y": 1 + }, + "widget": { + "name": "c4d87efe", + "queries": [ + { + "name": "dashboards/01ee564285a315dd80d473e76171660a/datasets/01ee564285a51daf810a8ffc5051bfee_tpep_dropoff_datetime", + "query": { + "datasetName": "ecfcdc7c", + "disaggregated": false, + "fields": [ + { + "expression": "`tpep_dropoff_datetime`", + "name": "tpep_dropoff_datetime" + }, + { + "expression": "COUNT_IF(`associative_filter_predicate_group`)", + "name": "tpep_dropoff_datetime_associativity" + } + ] + } + } + ], + "spec": { + "encodings": { + "fields": [ + { + "displayName": "tpep_dropoff_datetime", + "fieldName": "tpep_dropoff_datetime", + "queryName": "dashboards/01ee564285a315dd80d473e76171660a/datasets/01ee564285a51daf810a8ffc5051bfee_tpep_dropoff_datetime" + } + ] + }, + "frame": { + "showTitle": true, + "title": "Time Range" + }, + "version": 2, + "widgetType": "filter-date-range-picker" + } + } + }, + { + "position": { + "height": 4, + "width": 3, + "x": 0, + "y": 10 + }, + "widget": { + "name": "61e19e9c", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "ecfcdc7c", + "disaggregated": false, + "fields": [ + { + "expression": "COUNT(`*`)", + "name": "count(*)" + }, + { + "expression": "DATE_TRUNC(\"HOUR\", `tpep_pickup_datetime`)", + "name": "hourly(tpep_pickup_datetime)" + } + ] + } + } + ], + "spec": { + "encodings": { + "label": { + "show": false + }, + "x": { + "axis": { + "title": "Pickup Hour" + }, + "displayName": "Pickup Hour", + "fieldName": "hourly(tpep_pickup_datetime)", + "scale": { + "type": "temporal" + } + }, + "y": { + "axis": { + "title": "Number of Rides" + }, + "displayName": "Number of Rides", + "fieldName": "count(*)", + "scale": { + "type": "quantitative" + } + } + }, + "frame": { + "showTitle": true, + "title": "Pickup Hour Distribution" + }, + "mark": { + "colors": [ + "#077A9D", + "#FFAB00", + "#00A972", + "#FF3621", + "#8BCAE7", + "#AB4057", + "#99DDB4", + "#FCA4A1", + "#919191", + "#BF7080" + ] + }, + "version": 3, + "widgetType": "bar" + } + } + }, + { + "position": { + "height": 8, + "width": 4, + "x": 2, + "y": 2 + }, + "widget": { + "name": "3b1dff20", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "ecfcdc7c", + "disaggregated": true, + "fields": [ + { + "expression": "`day_of_week`", + "name": "day_of_week" + }, + { + "expression": "`fare_amount`", + "name": "fare_amount" + }, + { + "expression": "`trip_distance`", + "name": "trip_distance" + } + ] + } + } + ], + "spec": { + "encodings": { + "color": { + "displayName": "Day of Week", + "fieldName": "day_of_week", + "scale": { + "type": "categorical" + } + }, + "x": { + "axis": { + "title": "Trip Distance (miles)" + }, + "displayName": "trip_distance", + "fieldName": "trip_distance", + "scale": { + "type": "quantitative" + } + }, + "y": { + "axis": { + "title": "Fare Amount (USD)" + }, + "displayName": "fare_amount", + "fieldName": "fare_amount", + "scale": { + "type": "quantitative" + } + } + }, + "frame": { + "showTitle": true, + "title": "Daily Fare Trends by Day of Week" + }, + "version": 3, + "widgetType": "scatter" + } + } + }, + { + "position": { + "height": 1, + "width": 6, + "x": 0, + "y": 0 + }, + "widget": { + "name": "bd82f575", + "textbox_spec": "# NYC Taxi Trip Analysis" + } + }, + { + "position": { + "height": 4, + "width": 3, + "x": 3, + "y": 10 + }, + "widget": { + "name": "e7b33e79", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "ecfcdc7c", + "disaggregated": false, + "fields": [ + { + "expression": "COUNT(`*`)", + "name": "count(*)" + }, + { + "expression": "DATE_TRUNC(\"HOUR\", `tpep_dropoff_datetime`)", + "name": "hourly(tpep_dropoff_datetime)" + } + ] + } + } + ], + "spec": { + "encodings": { + "x": { + "axis": { + "title": "Dropoff Hour" + }, + "displayName": "Dropoff Hour", + "fieldName": "hourly(tpep_dropoff_datetime)", + "scale": { + "type": "temporal" + } + }, + "y": { + "axis": { + "title": "Number of Rides" + }, + "displayName": "Number of Rides", + "fieldName": "count(*)", + "scale": { + "type": "quantitative" + } + } + }, + "frame": { + "showTitle": true, + "title": "Dropoff Hour Distribution" + }, + "mark": { + "colors": [ + "#FFAB00", + "#FFAB00", + "#00A972", + "#FF3621", + "#8BCAE7", + "#AB4057", + "#99DDB4", + "#FCA4A1", + "#919191", + "#BF7080" + ] + }, + "version": 3, + "widgetType": "bar" + } + } + }, + { + "position": { + "height": 2, + "width": 2, + "x": 0, + "y": 2 + }, + "widget": { + "name": "299e756c", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "ecfcdc7c", + "disaggregated": false, + "fields": [ + { + "expression": "COUNT(`*`)", + "name": "count(*)" + } + ] + } + } + ], + "spec": { + "encodings": { + "value": { + "displayName": "Count of Records", + "fieldName": "count(*)", + "style": { + "bold": true, + "color": "#E92828" + } + } + }, + "frame": { + "showTitle": true, + "title": "Total Trips" + }, + "version": 2, + "widgetType": "counter" + } + } + }, + { + "position": { + "height": 1, + "width": 2, + "x": 2, + "y": 1 + }, + "widget": { + "name": "61a54236", + "queries": [ + { + "name": "dashboards/01eed0e4082a1c7e903cac7e74114376/datasets/01eed0e4082d1205adc131b86b10198e_pickup_zip", + "query": { + "datasetName": "fdefd57c", + "disaggregated": false, + "fields": [ + { + "expression": "`pickup_zip`", + "name": "pickup_zip" + }, + { + "expression": "COUNT_IF(`associative_filter_predicate_group`)", + "name": "pickup_zip_associativity" + } + ] + } + }, + { + "name": "dashboards/01eed0e4082a1c7e903cac7e74114376/datasets/01eed0e4082e1ff49c3209776820e82e_pickup_zip", + "query": { + "datasetName": "ecfcdc7c", + "disaggregated": false, + "fields": [ + { + "expression": "`pickup_zip`", + "name": "pickup_zip" + }, + { + "expression": "COUNT_IF(`associative_filter_predicate_group`)", + "name": "pickup_zip_associativity" + } + ] + } + } + ], + "spec": { + "encodings": { + "fields": [ + { + "displayName": "pickup_zip", + "fieldName": "pickup_zip", + "queryName": "dashboards/01eed0e4082a1c7e903cac7e74114376/datasets/01eed0e4082e1ff49c3209776820e82e_pickup_zip" + }, + { + "displayName": "pickup_zip", + "fieldName": "pickup_zip", + "queryName": "dashboards/01eed0e4082a1c7e903cac7e74114376/datasets/01eed0e4082d1205adc131b86b10198e_pickup_zip" + } + ] + }, + "frame": { + "showTitle": true, + "title": "Pickup Zip" + }, + "version": 2, + "widgetType": "filter-multi-select" + } + } + }, + { + "position": { + "height": 6, + "width": 2, + "x": 0, + "y": 4 + }, + "widget": { + "name": "985e7eb4", + "queries": [ + { + "name": "main_query", + "query": { + "datasetName": "fdefd57c", + "disaggregated": true, + "fields": [ + { + "expression": "`Number Trips`", + "name": "Number Trips" + }, + { + "expression": "`Route`", + "name": "Route" + }, + { + "expression": "`Total Revenue`", + "name": "Total Revenue" + } + ] + } + } + ], + "spec": { + "allowHTMLByDefault": false, + "condensed": true, + "encodings": { + "columns": [ + { + "alignContent": "left", + "allowHTML": false, + "allowSearch": false, + "booleanValues": [ + "false", + "true" + ], + "displayAs": "string", + "displayName": "Route", + "fieldName": "Route", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "order": 100000, + "preserveWhitespace": false, + "title": "Route", + "type": "string", + "useMonospaceFont": false, + "visible": true + }, + { + "alignContent": "right", + "allowHTML": false, + "allowSearch": false, + "booleanValues": [ + "false", + "true" + ], + "displayAs": "number", + "displayName": "Number Trips", + "fieldName": "Number Trips", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "numberFormat": "0", + "order": 100001, + "preserveWhitespace": false, + "title": "Number Trips", + "type": "integer", + "useMonospaceFont": false, + "visible": true + }, + { + "alignContent": "right", + "allowHTML": false, + "allowSearch": false, + "booleanValues": [ + "false", + "true" + ], + "cellFormat": { + "default": { + "foregroundColor": "#85CADE" + }, + "rules": [ + { + "if": { + "column": "Total Revenue", + "fn": "\u003c", + "literal": "51" + }, + "value": { + "foregroundColor": "#9C2638" + } + }, + { + "if": { + "column": "Total Revenue", + "fn": "\u003c", + "literal": "101" + }, + "value": { + "foregroundColor": "#FFD465" + } + }, + { + "if": { + "column": "Total Revenue", + "fn": "\u003c", + "literal": "6001" + }, + "value": { + "foregroundColor": "#1FA873" + } + } + ] + }, + "displayAs": "number", + "displayName": "Total Revenue", + "fieldName": "Total Revenue", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "numberFormat": "$0.00", + "order": 100002, + "preserveWhitespace": false, + "title": "Total Revenue", + "type": "float", + "useMonospaceFont": false, + "visible": true + } + ] + }, + "frame": { + "showTitle": true, + "title": "Route Revenue Attribution" + }, + "invisibleColumns": [ + { + "alignContent": "right", + "allowHTML": false, + "allowSearch": false, + "booleanValues": [ + "false", + "true" + ], + "displayAs": "number", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "pickup_zip", + "numberFormat": "0", + "order": 100000, + "preserveWhitespace": false, + "title": "pickup_zip", + "type": "integer", + "useMonospaceFont": false + }, + { + "alignContent": "right", + "allowHTML": false, + "allowSearch": false, + "booleanValues": [ + "false", + "true" + ], + "displayAs": "number", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "dropoff_zip", + "numberFormat": "0", + "order": 100001, + "preserveWhitespace": false, + "title": "dropoff_zip", + "type": "integer", + "useMonospaceFont": false + } + ], + "itemsPerPage": 25, + "paginationSize": "default", + "version": 1, + "widgetType": "table", + "withRowNumber": false + } + } + }, + { + "position": { + "height": 1, + "width": 2, + "x": 4, + "y": 1 + }, + "widget": { + "name": "b346c038", + "queries": [ + { + "name": "dashboards/01eed0e4082a1c7e903cac7e74114376/datasets/01eed0e4082d1205adc131b86b10198e_dropoff_zip", + "query": { + "datasetName": "fdefd57c", + "disaggregated": false, + "fields": [ + { + "expression": "`dropoff_zip`", + "name": "dropoff_zip" + }, + { + "expression": "COUNT_IF(`associative_filter_predicate_group`)", + "name": "dropoff_zip_associativity" + } + ] + } + }, + { + "name": "dashboards/01eed0e4082a1c7e903cac7e74114376/datasets/01eed0e4082e1ff49c3209776820e82e_dropoff_zip", + "query": { + "datasetName": "ecfcdc7c", + "disaggregated": false, + "fields": [ + { + "expression": "`dropoff_zip`", + "name": "dropoff_zip" + }, + { + "expression": "COUNT_IF(`associative_filter_predicate_group`)", + "name": "dropoff_zip_associativity" + } + ] + } + } + ], + "spec": { + "encodings": { + "fields": [ + { + "displayName": "dropoff_zip", + "fieldName": "dropoff_zip", + "queryName": "dashboards/01eed0e4082a1c7e903cac7e74114376/datasets/01eed0e4082e1ff49c3209776820e82e_dropoff_zip" + }, + { + "displayName": "dropoff_zip", + "fieldName": "dropoff_zip", + "queryName": "dashboards/01eed0e4082a1c7e903cac7e74114376/datasets/01eed0e4082d1205adc131b86b10198e_dropoff_zip" + } + ] + }, + "frame": { + "showTitle": true, + "title": "Dropoff Zip" + }, + "version": 2, + "widgetType": "filter-multi-select" + } + } + } + ], + "name": "b51b1363" + } + ] +} \ No newline at end of file diff --git a/tmp/dashboard-generate/databricks.yml b/tmp/dashboard-generate/databricks.yml new file mode 100644 index 0000000000..8670872cea --- /dev/null +++ b/tmp/dashboard-generate/databricks.yml @@ -0,0 +1,16 @@ +bundle: + name: dashboard-eng-work-generate + +workspace: + host: https://e2-dogfood.staging.cloud.databricks.com + +include: + - resources/*.yml + +permissions: + - group_name: users + level: CAN_VIEW + +targets: + dev: + mode: development diff --git a/tmp/dashboard-generate/resources/nyc_taxi_trip_analysis.yml b/tmp/dashboard-generate/resources/nyc_taxi_trip_analysis.yml new file mode 100644 index 0000000000..9a76969a12 --- /dev/null +++ b/tmp/dashboard-generate/resources/nyc_taxi_trip_analysis.yml @@ -0,0 +1,23 @@ +resources: + dashboards: + nyc_taxi_trip_analysis: + display_name: "NYC Taxi Trip Analysis" + parent_path: ${workspace.file_path} + warehouse_id: 4fe75792cd0d304c + definition_path: ../dashboards/nyc_taxi_trip_analysis.lvdash.json + + # To be implemented when ready in the product: + # + # catalog: ${var.default_catalog} + # schema: ${var.default_schema} + # schedules: + # - name: Daily + # # ... + permissions: + # Allow all users to view the dashboard + - group_name: users + level: CAN_READ + + # Allow all account users to view the dashboard + - group_name: account users + level: CAN_READ