dash_kit 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +170 -0
  4. data/Rakefile +18 -0
  5. data/app/assets/builds/dash_kit/dashboard_grid.css +1 -0
  6. data/app/assets/builds/dash_kit/dashboard_grid.js +5 -0
  7. data/app/assets/javascripts/dash_kit/index.js +2 -0
  8. data/app/assets/stylesheets/dash_kit/application.css +15 -0
  9. data/app/controllers/dash_kit/application_controller.rb +6 -0
  10. data/app/controllers/dash_kit/dashboards_controller.rb +139 -0
  11. data/app/controllers/dash_kit/widget_definitions_controller.rb +33 -0
  12. data/app/controllers/dash_kit/widgets_controller.rb +39 -0
  13. data/app/helpers/dash_kit/application_helper.rb +4 -0
  14. data/app/helpers/dash_kit/dashboard_helper.rb +130 -0
  15. data/app/javascript/dash_kit/dashboard_grid.jsx +10 -0
  16. data/app/models/concerns/dash_kit/widget_management.rb +16 -0
  17. data/app/models/dash_kit/application_record.rb +5 -0
  18. data/app/models/dash_kit/dashboard.rb +53 -0
  19. data/app/models/dash_kit/widget_definition.rb +9 -0
  20. data/app/views/dash_kit/dashboards/_widgets.html.erb +1 -0
  21. data/app/views/dash_kit/dashboards/edit.html.erb +21 -0
  22. data/app/views/dash_kit/dashboards/index.html.erb +34 -0
  23. data/app/views/dash_kit/dashboards/new.html.erb +28 -0
  24. data/app/views/dash_kit/widget_definitions/_missing_renderer.html.erb +3 -0
  25. data/app/views/layouts/dash_kit/application.html.erb +17 -0
  26. data/config/importmap.rb +1 -0
  27. data/config/routes.rb +16 -0
  28. data/lib/dash_kit/configuration_backfill.rb +27 -0
  29. data/lib/dash_kit/engine.rb +19 -0
  30. data/lib/dash_kit/reference/guide.md +289 -0
  31. data/lib/dash_kit/reference.rb +11 -0
  32. data/lib/dash_kit/renderer_registry.rb +21 -0
  33. data/lib/dash_kit/the_local/agents/dash_kit-develop.md +298 -0
  34. data/lib/dash_kit/the_local/agents/dash_kit-info.md +298 -0
  35. data/lib/dash_kit/the_local/agents/dash_kit-install.md +298 -0
  36. data/lib/dash_kit/the_local.rb +34 -0
  37. data/lib/dash_kit/version.rb +3 -0
  38. data/lib/dash_kit/widget_registry.rb +69 -0
  39. data/lib/dash_kit.rb +72 -0
  40. data/lib/generators/dash_kit/install_generator.rb +79 -0
  41. data/lib/generators/dash_kit/templates/create_dash_kit_dashboards.rb.tt +20 -0
  42. data/lib/generators/dash_kit/templates/create_dash_kit_widget_definitions.rb.tt +12 -0
  43. data/lib/generators/dash_kit/templates/dash_kit.rb.tt +16 -0
  44. data/lib/tasks/dash_kit_tasks.rake +9 -0
  45. metadata +147 -0
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DashKit
4
+ class WidgetsController < DashKit.parent_controller.constantize
5
+ def show
6
+ widget_key = params[:id].to_sym
7
+ widget_def = find_widget_definition(widget_key)
8
+
9
+ if widget_def.nil?
10
+ head :not_found
11
+ else
12
+ render partial: widget_def[:partial], layout: false, locals: { filter_state: filter_state }
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def filter_state
19
+ dashboard = dashboard_scope.find_by(id: params[:dashboard_id])
20
+ dashboard ? dashboard.filter_state : {}
21
+ end
22
+
23
+ def dashboard_scope
24
+ if DashKit.current_owner_method
25
+ DashKit::Dashboard.for_owner(send(DashKit.current_owner_method))
26
+ else
27
+ DashKit::Dashboard
28
+ end
29
+ end
30
+
31
+ def find_widget_definition(key)
32
+ DashKit.registry.dashboard_types.each do |type|
33
+ widgets = DashKit.registry.widgets_for(type)
34
+ return widgets[key] if widgets.key?(key)
35
+ end
36
+ nil
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,4 @@
1
+ module DashKit
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ks_blocks/layout_helper"
4
+ require "ks_blocks/content_helper"
5
+ require "keystone_ui/react/mount_helper"
6
+
7
+ module DashKit
8
+ module DashboardHelper
9
+ include KsBlocks::LayoutHelper
10
+ include KsBlocks::ContentHelper
11
+ include KeystoneUi::React::MountHelper
12
+
13
+ def dash_kit_viewable?(config)
14
+ DashKit.viewable?(config, dash_kit_current_viewer)
15
+ end
16
+
17
+ def dash_kit_editable?(config)
18
+ DashKit.editable?(config, dash_kit_current_viewer)
19
+ end
20
+
21
+ def dash_kit_shareable?(config)
22
+ DashKit.shareable?(config, dash_kit_current_viewer)
23
+ end
24
+
25
+ def dash_kit_current_viewer
26
+ method = DashKit.current_viewer_method || DashKit.current_owner_method
27
+ method && controller.send(method)
28
+ end
29
+
30
+ def dash_kit_available_sources
31
+ DashKit.available_sources(dash_kit_current_viewer)
32
+ end
33
+
34
+ def dash_kit_visualizations
35
+ DashKit.visualizations
36
+ end
37
+
38
+ def dash_kit_widget_label(config, widget_key)
39
+ config.available_widgets.dig(widget_key.to_sym, :label) || widget_key.to_s.humanize
40
+ end
41
+
42
+ def dash_kit_render_widgets(config:)
43
+ return content_tag(:div, "", id: "dashboard-widgets") unless config
44
+
45
+ content_tag(:div, id: dash_kit_widgets_id(config)) do
46
+ dash_kit_arrangeable?(config) ? dash_kit_widget_grid(config) : dash_kit_drawn_widgets(config)
47
+ end
48
+ end
49
+
50
+ def dash_kit_widgets_id(config)
51
+ "dashboard-widgets-#{config.id}"
52
+ end
53
+
54
+ def dash_kit_arrangeable?(config)
55
+ DashKit.editable?(config, dash_kit_current_viewer)
56
+ end
57
+
58
+ def dash_kit_widget_grid(config)
59
+ safe_join([
60
+ react_ui("dash_kit/dashboard-grid", config.layout_data.merge(base: dash_kit.dashboard_path(config), token: dash_kit_form_token)),
61
+ block_contents(config.blocks) { |block| dash_kit_block_frame(config, block) }
62
+ ])
63
+ end
64
+
65
+ def dash_kit_form_token
66
+ controller.respond_to?(:form_authenticity_token) ? controller.send(:form_authenticity_token) : nil
67
+ end
68
+
69
+ def dash_kit_drawn_widgets(config)
70
+ block_layout(config.blocks, kind: config.dashboard_type.to_sym) do |block|
71
+ dash_kit_block_frame(config, block)
72
+ end
73
+ end
74
+
75
+ def dash_kit_block_frame(config, block)
76
+ return dash_kit_widget_frame(block["type"], dashboard_id: config.id) unless block["type"] == WidgetRegistry::BUILT_WIDGET.to_s
77
+
78
+ dash_kit_widget_definition_frame(config.widget_definitions.find(block.dig("content", "definition_id")))
79
+ end
80
+
81
+ def dash_kit_widget_frame(widget_key, dashboard_id: nil, &block)
82
+ dash_kit_turbo_frame(
83
+ id: "widget_#{widget_key}",
84
+ src: dash_kit.widget_path(widget_key, dashboard_id: dashboard_id),
85
+ &block
86
+ )
87
+ end
88
+
89
+ def dash_kit_widget_definition_frame(definition, &block)
90
+ dash_kit_turbo_frame(
91
+ id: "widget_definition_#{definition.id}",
92
+ src: dash_kit.widget_definition_path(definition, dashboard_id: definition.dashboard_id),
93
+ &block
94
+ )
95
+ end
96
+
97
+ def dash_kit_turbo_frame(id:, src:, &block)
98
+ loading_content = block ? capture(&block) : dash_kit_loading_skeleton
99
+
100
+ content_tag("turbo-frame", loading_content, id: id, src: src, loading: "lazy", target: "_top", style: "display: block")
101
+ end
102
+
103
+ def dash_kit_filter_select(config:, key:, options:)
104
+ form_tag dash_kit.save_filters_dashboard_path(config), method: :post do
105
+ safe_join([
106
+ hidden_field_tag("filter_key", key),
107
+ select_tag("filter_value", options_for_select(options, config.filter_state[key.to_s]),
108
+ onchange: "this.form.requestSubmit()")
109
+ ])
110
+ end
111
+ end
112
+
113
+ def dash_kit_loading_skeleton
114
+ content_tag(:div, class: "rounded-xl bg-white dark:bg-zinc-800 shadow p-4") do
115
+ content_tag(:div, class: "animate-pulse") do
116
+ safe_join([
117
+ content_tag(:div, "", class: "h-5 bg-gray-200 dark:bg-zinc-700 rounded w-1/3 mb-4"),
118
+ content_tag(:div, class: "space-y-3") do
119
+ safe_join([
120
+ content_tag(:div, "", class: "h-4 bg-gray-200 dark:bg-zinc-700 rounded w-full"),
121
+ content_tag(:div, "", class: "h-4 bg-gray-200 dark:bg-zinc-700 rounded w-5/6"),
122
+ content_tag(:div, "", class: "h-4 bg-gray-200 dark:bg-zinc-700 rounded w-4/6")
123
+ ])
124
+ end
125
+ ])
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,10 @@
1
+ import { createRoot } from "react-dom/client"
2
+ import "react-grid-layout/css/styles.css"
3
+ import "keystone_ui-blocks/src/block_grid.css"
4
+ import BlockGrid from "keystone_ui-blocks/src/BlockGrid.jsx"
5
+ import { register } from "keystone_ui-react/src/registry.js"
6
+ import { startMounting } from "keystone_ui-react/src/mounting.js"
7
+
8
+ register("dash_kit/dashboard-grid", BlockGrid)
9
+
10
+ startMounting(document, createRoot)
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DashKit
4
+ module WidgetManagement
5
+ extend ActiveSupport::Concern
6
+
7
+ def available_widgets
8
+ DashKit.registry.widgets_for(dashboard_type.to_sym)
9
+ end
10
+
11
+ def update_filter(key, value)
12
+ self.filter_state = filter_state.merge(key.to_s => value)
13
+ save!
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ module DashKit
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ks_blocks/layout"
4
+
5
+ module DashKit
6
+ class Dashboard < ApplicationRecord
7
+ include WidgetManagement
8
+ include KsBlocks::Layout
9
+
10
+ self.table_name = "dash_kit_dashboards"
11
+
12
+ block_layout :blocks, kind: :blocks
13
+
14
+ def block_layout_kind
15
+ dashboard_type.to_sym
16
+ end
17
+
18
+ def add_built_widget(definition, x: nil, y: nil)
19
+ built = KsBlocks.registry.block_types(kind: block_layout_kind).find { |type| type.key == WidgetRegistry::BUILT_WIDGET }
20
+ add_block(built, x: x, y: y)
21
+ fill_block(blocks.last["id"], { "definition_id" => definition.id })
22
+ end
23
+
24
+ VISIBILITIES = %w[private account].freeze
25
+
26
+ belongs_to :owner, polymorphic: true
27
+ belongs_to :account, optional: true
28
+
29
+ has_many :widget_definitions, -> { order(:id) }, dependent: :destroy
30
+
31
+ validates :name, presence: true
32
+ validates :dashboard_type, presence: true
33
+ validates :visibility, inclusion: { in: VISIBILITIES }
34
+
35
+ scope :for_account, ->(account) { where(account: account) }
36
+ scope :for_owner, ->(owner) { where(owner: owner) }
37
+
38
+ def activate!
39
+ transaction do
40
+ self.class.for_owner(owner).where.not(id: id).update_all(active: false)
41
+ update!(active: true)
42
+ end
43
+ end
44
+
45
+ def duplicate!
46
+ copy = dup
47
+ copy.name = "#{name} (copy)"
48
+ copy.active = false
49
+ copy.save!
50
+ copy
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DashKit
4
+ class WidgetDefinition < ApplicationRecord
5
+ self.table_name = "dash_kit_widget_definitions"
6
+
7
+ belongs_to :dashboard
8
+ end
9
+ end
@@ -0,0 +1 @@
1
+ <%= dash_kit_render_widgets(config: config) %>
@@ -0,0 +1,21 @@
1
+ <div class="mx-auto max-w-md px-4 py-6">
2
+ <h1 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Rename dashboard</h1>
3
+
4
+ <%= form_with url: dash_kit.dashboard_path(@dashboard), scope: :dashboard, method: :patch do |form| %>
5
+ <div class="space-y-4">
6
+ <div>
7
+ <%= form.label :name, class: "block text-sm font-medium text-gray-900 dark:text-white mb-1" %>
8
+ <%= form.text_field :name,
9
+ value: @dashboard.name,
10
+ class: "block w-full rounded-lg border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-gray-900 dark:text-white" %>
11
+ </div>
12
+
13
+ <div class="flex items-center gap-3">
14
+ <%= form.submit "Save",
15
+ class: "rounded-md bg-accent-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-accent-500" %>
16
+ <%= link_to "Cancel", dash_kit.dashboards_path,
17
+ class: "text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700" %>
18
+ </div>
19
+ </div>
20
+ <% end %>
21
+ </div>
@@ -0,0 +1,34 @@
1
+ <div class="mx-auto max-w-3xl px-4 py-6">
2
+ <div class="flex items-center justify-between mb-4">
3
+ <h1 class="text-xl font-semibold text-gray-900 dark:text-white">Dashboards</h1>
4
+ <%= link_to "New dashboard", dash_kit.new_dashboard_path,
5
+ class: "rounded-md bg-accent-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-accent-500" %>
6
+ </div>
7
+
8
+ <% if @dashboards.any? %>
9
+ <ul class="space-y-2">
10
+ <% @dashboards.each do |dashboard| %>
11
+ <li class="flex items-center justify-between p-3 rounded-lg border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800">
12
+ <span class="text-sm font-medium text-gray-900 dark:text-white"><%= dashboard.name %></span>
13
+ <div class="flex items-center gap-3">
14
+ <%= link_to "Rename", dash_kit.edit_dashboard_path(dashboard),
15
+ class: "text-xs font-semibold text-gray-500 dark:text-gray-400 hover:text-gray-700" %>
16
+ <%= button_to "Duplicate", dash_kit.duplicate_dashboard_path(dashboard),
17
+ class: "text-xs font-semibold text-gray-500 dark:text-gray-400 hover:text-gray-700" %>
18
+ <%= button_to "Delete", dash_kit.dashboard_path(dashboard), method: :delete,
19
+ form: { data: { turbo_confirm: "Delete this dashboard?" } },
20
+ class: "text-xs font-semibold text-red-600 dark:text-red-400 hover:text-red-700" %>
21
+ <% if dashboard.active? %>
22
+ <span class="text-xs font-semibold text-accent-600 dark:text-accent-400">Active</span>
23
+ <% else %>
24
+ <%= button_to "Select", dash_kit.select_dashboard_path(dashboard),
25
+ class: "rounded-md border border-gray-300 dark:border-zinc-700 px-3 py-1 text-xs font-semibold text-gray-900 dark:text-white hover:bg-gray-50 dark:hover:bg-zinc-700" %>
26
+ <% end %>
27
+ </div>
28
+ </li>
29
+ <% end %>
30
+ </ul>
31
+ <% else %>
32
+ <p class="text-sm text-gray-500 dark:text-gray-400">No dashboards yet.</p>
33
+ <% end %>
34
+ </div>
@@ -0,0 +1,28 @@
1
+ <div class="mx-auto max-w-md px-4 py-6">
2
+ <h1 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">New dashboard</h1>
3
+
4
+ <%= form_with url: dash_kit.dashboards_path, scope: :dashboard, method: :post do |form| %>
5
+ <div class="space-y-4">
6
+ <div>
7
+ <%= form.label :name, class: "block text-sm font-medium text-gray-900 dark:text-white mb-1" %>
8
+ <%= form.text_field :name,
9
+ value: @dashboard.name,
10
+ class: "block w-full rounded-lg border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-gray-900 dark:text-white" %>
11
+ </div>
12
+
13
+ <div>
14
+ <%= form.label :dashboard_type, "Type", class: "block text-sm font-medium text-gray-900 dark:text-white mb-1" %>
15
+ <%= form.text_field :dashboard_type,
16
+ value: @dashboard.dashboard_type,
17
+ class: "block w-full rounded-lg border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-gray-900 dark:text-white" %>
18
+ </div>
19
+
20
+ <div class="flex items-center gap-3">
21
+ <%= form.submit "Create",
22
+ class: "rounded-md bg-accent-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-accent-500" %>
23
+ <%= link_to "Cancel", dash_kit.dashboards_path,
24
+ class: "text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700" %>
25
+ </div>
26
+ </div>
27
+ <% end %>
28
+ </div>
@@ -0,0 +1,3 @@
1
+ <%= ui_panel do %>
2
+ This widget can't be displayed.
3
+ <% end %>
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dash kit</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= yield :head %>
9
+
10
+ <%= stylesheet_link_tag "dash_kit/application", media: "all" %>
11
+ </head>
12
+ <body>
13
+
14
+ <%= yield %>
15
+
16
+ </body>
17
+ </html>
@@ -0,0 +1 @@
1
+ pin "dash_kit/index", to: "dash_kit/index.js"
data/config/routes.rb ADDED
@@ -0,0 +1,16 @@
1
+ DashKit::Engine.routes.draw do
2
+ resources :widgets, only: [ :show ]
3
+ resources :widget_definitions, only: [ :show ]
4
+
5
+ resources :dashboards, only: [ :index, :new, :create, :edit, :update, :destroy ] do
6
+ member do
7
+ post :select
8
+ post :duplicate
9
+ post :save_filters
10
+ patch "blocks", to: "dashboards#place_blocks", as: :blocks
11
+ post :create_definition
12
+ post :update_definition
13
+ post :destroy_definition
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DashKit
4
+ module ConfigurationBackfill
5
+ class LegacyConfiguration < ActiveRecord::Base
6
+ self.table_name = "dash_kit_configurations"
7
+ end
8
+
9
+ def self.run
10
+ return unless LegacyConfiguration.table_exists?
11
+
12
+ LegacyConfiguration.find_each do |config|
13
+ Dashboard.find_or_create_by!(
14
+ owner_type: config.owner_type,
15
+ owner_id: config.owner_id,
16
+ dashboard_type: config.dashboard_type,
17
+ name: config.dashboard_type.to_s.humanize
18
+ ) do |dashboard|
19
+ dashboard.widget_order = config.widget_order
20
+ dashboard.hidden_widgets = config.hidden_widgets
21
+ dashboard.filter_state = config.filter_state
22
+ dashboard.widget_settings = config.widget_settings
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module DashKit
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace DashKit
4
+
5
+ initializer "dash_kit.tailwind" do
6
+ next unless Gem.loaded_specs.key?("keystone_ui")
7
+
8
+ require "keystone_ui"
9
+ KeystoneUi.configuration.tailwind_sources << root.join("app/views/**/*.erb").to_s
10
+ KeystoneUi.configuration.tailwind_sources << root.join("app/assets/builds/dash_kit/*.js").to_s
11
+ end
12
+
13
+ initializer "dash_kit.importmap", before: "importmap" do |app|
14
+ if app.config.respond_to?(:importmap)
15
+ app.config.importmap.paths << root.join("config/importmap.rb")
16
+ end
17
+ end
18
+ end
19
+ end