easy_ml 0.1.4 → 0.2.0.pre.rc1

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 (239) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -26
  3. data/Rakefile +45 -0
  4. data/app/controllers/easy_ml/application_controller.rb +67 -0
  5. data/app/controllers/easy_ml/columns_controller.rb +38 -0
  6. data/app/controllers/easy_ml/datasets_controller.rb +156 -0
  7. data/app/controllers/easy_ml/datasources_controller.rb +88 -0
  8. data/app/controllers/easy_ml/deploys_controller.rb +20 -0
  9. data/app/controllers/easy_ml/models_controller.rb +151 -0
  10. data/app/controllers/easy_ml/retraining_runs_controller.rb +19 -0
  11. data/app/controllers/easy_ml/settings_controller.rb +59 -0
  12. data/app/frontend/components/AlertProvider.tsx +108 -0
  13. data/app/frontend/components/DatasetPreview.tsx +161 -0
  14. data/app/frontend/components/EmptyState.tsx +28 -0
  15. data/app/frontend/components/ModelCard.tsx +255 -0
  16. data/app/frontend/components/ModelDetails.tsx +334 -0
  17. data/app/frontend/components/ModelForm.tsx +384 -0
  18. data/app/frontend/components/Navigation.tsx +300 -0
  19. data/app/frontend/components/Pagination.tsx +72 -0
  20. data/app/frontend/components/Popover.tsx +55 -0
  21. data/app/frontend/components/PredictionStream.tsx +105 -0
  22. data/app/frontend/components/ScheduleModal.tsx +726 -0
  23. data/app/frontend/components/SearchInput.tsx +23 -0
  24. data/app/frontend/components/SearchableSelect.tsx +132 -0
  25. data/app/frontend/components/dataset/AutosaveIndicator.tsx +39 -0
  26. data/app/frontend/components/dataset/ColumnConfigModal.tsx +431 -0
  27. data/app/frontend/components/dataset/ColumnFilters.tsx +256 -0
  28. data/app/frontend/components/dataset/ColumnList.tsx +101 -0
  29. data/app/frontend/components/dataset/FeatureConfigPopover.tsx +57 -0
  30. data/app/frontend/components/dataset/FeaturePicker.tsx +205 -0
  31. data/app/frontend/components/dataset/PreprocessingConfig.tsx +704 -0
  32. data/app/frontend/components/dataset/SplitConfigurator.tsx +120 -0
  33. data/app/frontend/components/dataset/splitters/DateSplitter.tsx +58 -0
  34. data/app/frontend/components/dataset/splitters/KFoldSplitter.tsx +68 -0
  35. data/app/frontend/components/dataset/splitters/LeavePOutSplitter.tsx +29 -0
  36. data/app/frontend/components/dataset/splitters/PredefinedSplitter.tsx +146 -0
  37. data/app/frontend/components/dataset/splitters/RandomSplitter.tsx +85 -0
  38. data/app/frontend/components/dataset/splitters/StratifiedSplitter.tsx +79 -0
  39. data/app/frontend/components/dataset/splitters/constants.ts +77 -0
  40. data/app/frontend/components/dataset/splitters/types.ts +168 -0
  41. data/app/frontend/components/dataset/splitters/utils.ts +53 -0
  42. data/app/frontend/components/features/CodeEditor.tsx +46 -0
  43. data/app/frontend/components/features/DataPreview.tsx +150 -0
  44. data/app/frontend/components/features/FeatureCard.tsx +88 -0
  45. data/app/frontend/components/features/FeatureForm.tsx +235 -0
  46. data/app/frontend/components/features/FeatureGroupCard.tsx +54 -0
  47. data/app/frontend/components/settings/PluginSettings.tsx +81 -0
  48. data/app/frontend/components/ui/badge.tsx +44 -0
  49. data/app/frontend/components/ui/collapsible.tsx +9 -0
  50. data/app/frontend/components/ui/scroll-area.tsx +46 -0
  51. data/app/frontend/components/ui/separator.tsx +29 -0
  52. data/app/frontend/entrypoints/App.tsx +40 -0
  53. data/app/frontend/entrypoints/Application.tsx +24 -0
  54. data/app/frontend/hooks/useAutosave.ts +61 -0
  55. data/app/frontend/layouts/Layout.tsx +38 -0
  56. data/app/frontend/lib/utils.ts +6 -0
  57. data/app/frontend/mockData.ts +272 -0
  58. data/app/frontend/pages/DatasetDetailsPage.tsx +103 -0
  59. data/app/frontend/pages/DatasetsPage.tsx +261 -0
  60. data/app/frontend/pages/DatasourceFormPage.tsx +147 -0
  61. data/app/frontend/pages/DatasourcesPage.tsx +261 -0
  62. data/app/frontend/pages/EditModelPage.tsx +45 -0
  63. data/app/frontend/pages/EditTransformationPage.tsx +56 -0
  64. data/app/frontend/pages/ModelsPage.tsx +115 -0
  65. data/app/frontend/pages/NewDatasetPage.tsx +366 -0
  66. data/app/frontend/pages/NewModelPage.tsx +45 -0
  67. data/app/frontend/pages/NewTransformationPage.tsx +43 -0
  68. data/app/frontend/pages/SettingsPage.tsx +272 -0
  69. data/app/frontend/pages/ShowModelPage.tsx +30 -0
  70. data/app/frontend/pages/TransformationsPage.tsx +95 -0
  71. data/app/frontend/styles/application.css +100 -0
  72. data/app/frontend/types/dataset.ts +146 -0
  73. data/app/frontend/types/datasource.ts +33 -0
  74. data/app/frontend/types/preprocessing.ts +1 -0
  75. data/app/frontend/types.ts +113 -0
  76. data/app/helpers/easy_ml/application_helper.rb +10 -0
  77. data/app/jobs/easy_ml/application_job.rb +21 -0
  78. data/app/jobs/easy_ml/batch_job.rb +46 -0
  79. data/app/jobs/easy_ml/compute_feature_job.rb +19 -0
  80. data/app/jobs/easy_ml/deploy_job.rb +13 -0
  81. data/app/jobs/easy_ml/finalize_feature_job.rb +15 -0
  82. data/app/jobs/easy_ml/refresh_dataset_job.rb +32 -0
  83. data/app/jobs/easy_ml/schedule_retraining_job.rb +11 -0
  84. data/app/jobs/easy_ml/sync_datasource_job.rb +17 -0
  85. data/app/jobs/easy_ml/training_job.rb +62 -0
  86. data/app/models/easy_ml/adapters/base_adapter.rb +45 -0
  87. data/app/models/easy_ml/adapters/polars_adapter.rb +77 -0
  88. data/app/models/easy_ml/cleaner.rb +82 -0
  89. data/app/models/easy_ml/column.rb +124 -0
  90. data/app/models/easy_ml/column_history.rb +30 -0
  91. data/app/models/easy_ml/column_list.rb +122 -0
  92. data/app/models/easy_ml/concerns/configurable.rb +61 -0
  93. data/app/models/easy_ml/concerns/versionable.rb +19 -0
  94. data/app/models/easy_ml/dataset.rb +767 -0
  95. data/app/models/easy_ml/dataset_history.rb +56 -0
  96. data/app/models/easy_ml/datasource.rb +182 -0
  97. data/app/models/easy_ml/datasource_history.rb +24 -0
  98. data/app/models/easy_ml/datasources/base_datasource.rb +54 -0
  99. data/app/models/easy_ml/datasources/file_datasource.rb +58 -0
  100. data/app/models/easy_ml/datasources/polars_datasource.rb +89 -0
  101. data/app/models/easy_ml/datasources/s3_datasource.rb +97 -0
  102. data/app/models/easy_ml/deploy.rb +114 -0
  103. data/app/models/easy_ml/event.rb +79 -0
  104. data/app/models/easy_ml/feature.rb +437 -0
  105. data/app/models/easy_ml/feature_history.rb +38 -0
  106. data/app/models/easy_ml/model.rb +575 -41
  107. data/app/models/easy_ml/model_file.rb +133 -0
  108. data/app/models/easy_ml/model_file_history.rb +24 -0
  109. data/app/models/easy_ml/model_history.rb +51 -0
  110. data/app/models/easy_ml/models/base_model.rb +58 -0
  111. data/app/models/easy_ml/models/hyperparameters/base.rb +99 -0
  112. data/app/models/easy_ml/models/hyperparameters/xgboost/dart.rb +82 -0
  113. data/app/models/easy_ml/models/hyperparameters/xgboost/gblinear.rb +82 -0
  114. data/app/models/easy_ml/models/hyperparameters/xgboost/gbtree.rb +97 -0
  115. data/app/models/easy_ml/models/hyperparameters/xgboost.rb +71 -0
  116. data/app/models/easy_ml/models/xgboost/evals_callback.rb +138 -0
  117. data/app/models/easy_ml/models/xgboost/progress_callback.rb +39 -0
  118. data/app/models/easy_ml/models/xgboost.rb +544 -5
  119. data/app/models/easy_ml/prediction.rb +44 -0
  120. data/app/models/easy_ml/retraining_job.rb +278 -0
  121. data/app/models/easy_ml/retraining_run.rb +184 -0
  122. data/app/models/easy_ml/settings.rb +37 -0
  123. data/app/models/easy_ml/splitter.rb +90 -0
  124. data/app/models/easy_ml/splitters/base_splitter.rb +28 -0
  125. data/app/models/easy_ml/splitters/date_splitter.rb +91 -0
  126. data/app/models/easy_ml/splitters/predefined_splitter.rb +74 -0
  127. data/app/models/easy_ml/splitters/random_splitter.rb +82 -0
  128. data/app/models/easy_ml/tuner_job.rb +56 -0
  129. data/app/models/easy_ml/tuner_run.rb +31 -0
  130. data/app/models/splitter_history.rb +6 -0
  131. data/app/serializers/easy_ml/column_serializer.rb +27 -0
  132. data/app/serializers/easy_ml/dataset_serializer.rb +73 -0
  133. data/app/serializers/easy_ml/datasource_serializer.rb +64 -0
  134. data/app/serializers/easy_ml/feature_serializer.rb +27 -0
  135. data/app/serializers/easy_ml/model_serializer.rb +90 -0
  136. data/app/serializers/easy_ml/retraining_job_serializer.rb +22 -0
  137. data/app/serializers/easy_ml/retraining_run_serializer.rb +39 -0
  138. data/app/serializers/easy_ml/settings_serializer.rb +9 -0
  139. data/app/views/layouts/easy_ml/application.html.erb +15 -0
  140. data/config/initializers/resque.rb +3 -0
  141. data/config/resque-pool.yml +6 -0
  142. data/config/routes.rb +39 -0
  143. data/config/spring.rb +1 -0
  144. data/config/vite.json +15 -0
  145. data/lib/easy_ml/configuration.rb +64 -0
  146. data/lib/easy_ml/core/evaluators/base_evaluator.rb +53 -0
  147. data/lib/easy_ml/core/evaluators/classification_evaluators.rb +126 -0
  148. data/lib/easy_ml/core/evaluators/regression_evaluators.rb +66 -0
  149. data/lib/easy_ml/core/model_evaluator.rb +161 -89
  150. data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +28 -18
  151. data/lib/easy_ml/core/tuner/adapters/xgboost_adapter.rb +4 -25
  152. data/lib/easy_ml/core/tuner.rb +123 -62
  153. data/lib/easy_ml/core.rb +0 -3
  154. data/lib/easy_ml/core_ext/hash.rb +24 -0
  155. data/lib/easy_ml/core_ext/pathname.rb +11 -5
  156. data/lib/easy_ml/data/date_converter.rb +90 -0
  157. data/lib/easy_ml/data/filter_extensions.rb +31 -0
  158. data/lib/easy_ml/data/polars_column.rb +126 -0
  159. data/lib/easy_ml/data/polars_reader.rb +297 -0
  160. data/lib/easy_ml/data/preprocessor.rb +280 -142
  161. data/lib/easy_ml/data/simple_imputer.rb +255 -0
  162. data/lib/easy_ml/data/splits/file_split.rb +252 -0
  163. data/lib/easy_ml/data/splits/in_memory_split.rb +54 -0
  164. data/lib/easy_ml/data/splits/split.rb +95 -0
  165. data/lib/easy_ml/data/splits.rb +9 -0
  166. data/lib/easy_ml/data/statistics_learner.rb +93 -0
  167. data/lib/easy_ml/data/synced_directory.rb +341 -0
  168. data/lib/easy_ml/data.rb +6 -2
  169. data/lib/easy_ml/engine.rb +105 -6
  170. data/lib/easy_ml/feature_store.rb +227 -0
  171. data/lib/easy_ml/features.rb +61 -0
  172. data/lib/easy_ml/initializers/inflections.rb +17 -3
  173. data/lib/easy_ml/logging.rb +2 -2
  174. data/lib/easy_ml/predict.rb +74 -0
  175. data/lib/easy_ml/railtie/generators/migration/migration_generator.rb +192 -36
  176. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_column_histories.rb.tt +9 -0
  177. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_columns.rb.tt +25 -0
  178. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_dataset_histories.rb.tt +9 -0
  179. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasets.rb.tt +31 -0
  180. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasource_histories.rb.tt +9 -0
  181. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasources.rb.tt +16 -0
  182. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_deploys.rb.tt +24 -0
  183. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_events.rb.tt +20 -0
  184. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_feature_histories.rb.tt +14 -0
  185. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_features.rb.tt +32 -0
  186. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_file_histories.rb.tt +9 -0
  187. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_files.rb.tt +17 -0
  188. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_histories.rb.tt +9 -0
  189. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_models.rb.tt +20 -9
  190. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_predictions.rb.tt +17 -0
  191. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_retraining_jobs.rb.tt +77 -0
  192. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_settings.rb.tt +9 -0
  193. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitter_histories.rb.tt +9 -0
  194. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitters.rb.tt +15 -0
  195. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_tuner_jobs.rb.tt +40 -0
  196. data/lib/easy_ml/support/est.rb +5 -1
  197. data/lib/easy_ml/support/file_rotate.rb +79 -15
  198. data/lib/easy_ml/support/file_support.rb +9 -0
  199. data/lib/easy_ml/support/local_file.rb +24 -0
  200. data/lib/easy_ml/support/lockable.rb +62 -0
  201. data/lib/easy_ml/support/synced_file.rb +103 -0
  202. data/lib/easy_ml/support/utc.rb +5 -1
  203. data/lib/easy_ml/support.rb +6 -3
  204. data/lib/easy_ml/version.rb +4 -1
  205. data/lib/easy_ml.rb +7 -2
  206. metadata +355 -72
  207. data/app/models/easy_ml/models.rb +0 -5
  208. data/lib/easy_ml/core/model.rb +0 -30
  209. data/lib/easy_ml/core/model_core.rb +0 -181
  210. data/lib/easy_ml/core/models/hyperparameters/base.rb +0 -34
  211. data/lib/easy_ml/core/models/hyperparameters/xgboost.rb +0 -19
  212. data/lib/easy_ml/core/models/xgboost.rb +0 -10
  213. data/lib/easy_ml/core/models/xgboost_core.rb +0 -220
  214. data/lib/easy_ml/core/models.rb +0 -10
  215. data/lib/easy_ml/core/uploaders/model_uploader.rb +0 -24
  216. data/lib/easy_ml/core/uploaders.rb +0 -7
  217. data/lib/easy_ml/data/dataloader.rb +0 -6
  218. data/lib/easy_ml/data/dataset/data/preprocessor/statistics.json +0 -31
  219. data/lib/easy_ml/data/dataset/data/sample_info.json +0 -1
  220. data/lib/easy_ml/data/dataset/dataset/files/sample_info.json +0 -1
  221. data/lib/easy_ml/data/dataset/splits/file_split.rb +0 -140
  222. data/lib/easy_ml/data/dataset/splits/in_memory_split.rb +0 -49
  223. data/lib/easy_ml/data/dataset/splits/split.rb +0 -98
  224. data/lib/easy_ml/data/dataset/splits.rb +0 -11
  225. data/lib/easy_ml/data/dataset/splitters/date_splitter.rb +0 -43
  226. data/lib/easy_ml/data/dataset/splitters.rb +0 -9
  227. data/lib/easy_ml/data/dataset.rb +0 -430
  228. data/lib/easy_ml/data/datasource/datasource_factory.rb +0 -60
  229. data/lib/easy_ml/data/datasource/file_datasource.rb +0 -40
  230. data/lib/easy_ml/data/datasource/merged_datasource.rb +0 -64
  231. data/lib/easy_ml/data/datasource/polars_datasource.rb +0 -41
  232. data/lib/easy_ml/data/datasource/s3_datasource.rb +0 -89
  233. data/lib/easy_ml/data/datasource.rb +0 -33
  234. data/lib/easy_ml/data/preprocessor/preprocessor.rb +0 -205
  235. data/lib/easy_ml/data/preprocessor/simple_imputer.rb +0 -402
  236. data/lib/easy_ml/deployment.rb +0 -5
  237. data/lib/easy_ml/support/synced_directory.rb +0 -134
  238. data/lib/easy_ml/transforms.rb +0 -29
  239. /data/{lib/easy_ml/core → app/models/easy_ml}/models/hyperparameters.rb +0 -0
@@ -0,0 +1,88 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: easy_ml_datasources
4
+ #
5
+ # id :bigint not null, primary key
6
+ # name :string not null
7
+ # datasource_type :string
8
+ # root_dir :string
9
+ # configuration :json
10
+ # created_at :datetime not null
11
+ # updated_at :datetime not null
12
+ #
13
+ module EasyML
14
+ class DatasourcesController < ApplicationController
15
+ def index
16
+ @datasources = Datasource.all.order(id: :asc)
17
+ render inertia: "pages/DatasourcesPage", props: {
18
+ datasources: @datasources.map { |datasource| datasource_to_json(datasource) },
19
+ }
20
+ end
21
+
22
+ def show
23
+ @datasource = Datasource.find(params[:id])
24
+ render json: @datasource, serializer: DatasourceSerializer
25
+ end
26
+
27
+ def edit
28
+ datasource = EasyML::Datasource.find_by(id: params[:id])
29
+
30
+ render inertia: "pages/DatasourceFormPage", props: {
31
+ datasource: datasource_to_json(datasource),
32
+ constants: EasyML::Datasource.constants,
33
+ }
34
+ end
35
+
36
+ def new
37
+ render inertia: "pages/DatasourceFormPage", props: {
38
+ constants: EasyML::Datasource.constants,
39
+ }
40
+ end
41
+
42
+ def create
43
+ EasyML::Datasource.transaction do
44
+ datasource = EasyML::Datasource.create!(datasource_params)
45
+ end
46
+
47
+ redirect_to easy_ml_datasources_path, notice: "Datasource was successfully created."
48
+ rescue ActiveRecord::RecordInvalid => e
49
+ redirect_to new_easy_ml_datasource_path, alert: e.record.errors.full_messages.join(", ")
50
+ end
51
+
52
+ def destroy
53
+ @datasource = Datasource.find(params[:id])
54
+ @datasource.destroy
55
+
56
+ redirect_to easy_ml_datasources_path, notice: "Datasource was successfully deleted."
57
+ end
58
+
59
+ def update
60
+ datasource = EasyML::Datasource.find(params[:id])
61
+ if datasource.update(datasource_params)
62
+ redirect_to easy_ml_datasources_path, notice: "Datasource was successfully updated."
63
+ else
64
+ redirect_to edit_easy_ml_datasource_path(datasource), alert: datasource.errors.full_messages.join(", ")
65
+ end
66
+ end
67
+
68
+ def sync
69
+ datasource = Datasource.find(params[:id])
70
+ datasource.update(is_syncing: true)
71
+
72
+ # Start sync in background to avoid blocking
73
+ datasource.refresh_async
74
+
75
+ redirect_to easy_ml_datasources_path, notice: "Datasource is syncing..."
76
+ rescue ActiveRecord::RecordNotFound
77
+ redirect_to easy_ml_datasources_path, error: "Datasource not found..."
78
+ end
79
+
80
+ private
81
+
82
+ def datasource_params
83
+ params.require(:datasource).permit(:name, :s3_bucket, :s3_prefix, :s3_region, :datasource_type).merge!(
84
+ datasource_type: "s3",
85
+ )
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,20 @@
1
+ module EasyML
2
+ class DeploysController < ApplicationController
3
+ def create
4
+ run = EasyML::RetrainingRun.find(params[:retraining_run_id])
5
+ run.update(is_deploying: true)
6
+ @deploy = EasyML::Deploy.create!(
7
+ model_id: params[:easy_ml_model_id],
8
+ retraining_run_id: params[:retraining_run_id],
9
+ trigger: "manual",
10
+ )
11
+ @deploy.deploy
12
+
13
+ flash[:notice] = "Model deployment has started"
14
+ redirect_to easy_ml_model_path(@deploy.model)
15
+ rescue => e
16
+ flash[:alert] = "Trouble deploying model: #{e.message}"
17
+ redirect_to easy_ml_model_path(@deploy.model)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,151 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: easy_ml_models
4
+ #
5
+ # id :bigint not null, primary key
6
+ # name :string not null
7
+ # model_type :string
8
+ # status :string
9
+ # dataset_id :bigint
10
+ # configuration :json
11
+ # version :string not null
12
+ # root_dir :string
13
+ # file :json
14
+ # created_at :datetime not null
15
+ # updated_at :datetime not null
16
+ #
17
+ module EasyML
18
+ class ModelsController < ApplicationController
19
+ include EasyML::Engine.routes.url_helpers
20
+
21
+ def index
22
+ models = EasyML::Model.all.includes(includes_list).order(:last_trained_at, :id)
23
+
24
+ render inertia: "pages/ModelsPage", props: {
25
+ models: models.map { |model| model_to_json(model) },
26
+ }
27
+ end
28
+
29
+ def new
30
+ render inertia: "pages/NewModelPage", props: {
31
+ datasets: EasyML::Dataset.all.map do |dataset|
32
+ dataset.slice(:id, :name, :num_rows)
33
+ end,
34
+ constants: EasyML::Model.constants,
35
+ }
36
+ end
37
+
38
+ def edit
39
+ model = Model.includes(includes_list).find(params[:id])
40
+ render inertia: "pages/EditModelPage", props: {
41
+ model: model_to_json(model),
42
+ datasets: EasyML::Dataset.all.map do |dataset|
43
+ dataset.slice(:id, :name, :num_rows)
44
+ end,
45
+ constants: EasyML::Model.constants,
46
+ }
47
+ end
48
+
49
+ def create
50
+ model = Model.new(model_params)
51
+
52
+ if model.save
53
+ flash[:notice] = "Model was successfully created."
54
+ redirect_to easy_ml_models_path
55
+ else
56
+ render inertia: "pages/NewModelPage", props: {
57
+ datasets: EasyML::Dataset.all.map do |dataset|
58
+ dataset.slice(:id, :name, :num_rows)
59
+ end,
60
+ constants: EasyML::Model.constants,
61
+ errors: model.errors.to_hash(true),
62
+ }
63
+ end
64
+ end
65
+
66
+ def update
67
+ model = Model.find(params[:id])
68
+
69
+ if model.update(model_params)
70
+ flash[:notice] = "Model was successfully updated."
71
+ redirect_to easy_ml_models_path
72
+ else
73
+ render inertia: "pages/EditModelPage", props: {
74
+ model: model_to_json(model),
75
+ datasets: EasyML::Dataset.all.map { |dataset| dataset_to_json(dataset) },
76
+ constants: EasyML::Model.constants,
77
+ errors: model.errors.to_hash(true),
78
+ }
79
+ end
80
+ end
81
+
82
+ def show
83
+ model = Model.includes(includes_list)
84
+ .find(params[:id])
85
+
86
+ if request.format.json?
87
+ render json: { model: model_to_json(model) }
88
+ else
89
+ render inertia: "pages/ShowModelPage", props: {
90
+ model: model_to_json(model),
91
+ }
92
+ end
93
+ end
94
+
95
+ def destroy
96
+ model = Model.find(params[:id])
97
+
98
+ if model.destroy
99
+ flash[:notice] = "Model was successfully deleted."
100
+ redirect_to easy_ml_models_path
101
+ else
102
+ flash[:alert] = "Failed to delete the model."
103
+ redirect_to easy_ml_models_path
104
+ end
105
+ end
106
+
107
+ def train
108
+ model = EasyML::Model.find(params[:id])
109
+ model.train
110
+ flash[:notice] = "Model training started!"
111
+
112
+ redirect_to easy_ml_models_path
113
+ end
114
+
115
+ private
116
+
117
+ def includes_list
118
+ [:retraining_runs, :retraining_job, dataset: [:columns, :features, :splitter]]
119
+ end
120
+
121
+ def model_params
122
+ params.require(:model).permit(
123
+ :name,
124
+ :model_type,
125
+ :dataset_id,
126
+ :task,
127
+ :objective,
128
+ metrics: [],
129
+ retraining_job_attributes: [
130
+ :id,
131
+ :frequency,
132
+ :active,
133
+ :metric,
134
+ :direction,
135
+ :threshold,
136
+ :tuning_frequency,
137
+ :batch_mode,
138
+ :batch_size,
139
+ :batch_overlap,
140
+ :batch_key,
141
+ :tuning_enabled,
142
+ at: [:hour, :day_of_week, :day_of_month],
143
+ tuner_config: [
144
+ :n_trials,
145
+ config: {},
146
+ ],
147
+ ],
148
+ )
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,19 @@
1
+ module EasyML
2
+ class RetrainingRunsController < ApplicationController
3
+ def index
4
+ model = EasyML::Model.find(params[:id])
5
+ limit = (params[:limit] || 20).to_i
6
+ offset = (params[:offset] || 0).to_i
7
+
8
+ render json: ModelSerializer.new(
9
+ model,
10
+ params: { limit: limit, offset: offset },
11
+ ).serializable_hash
12
+ end
13
+
14
+ def show
15
+ run = EasyML::RetrainingRun.find(params[:id])
16
+ render json: RetrainingRunSerializer.new(run).serializable_hash
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,59 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: easy_ml_settings
4
+ #
5
+ # id :bigint not null, primary key
6
+ # storage :string
7
+ # timezone :string
8
+ # s3_access_key_id :string
9
+ # s3_secret_access_key :string
10
+ # s3_bucket :string
11
+ # s3_region :string
12
+ # s3_prefix :string
13
+ # created_at :datetime not null
14
+ # updated_at :datetime not null
15
+ #
16
+ module EasyML
17
+ class SettingsController < ApplicationController
18
+ def index
19
+ @settings = Settings.first_or_create
20
+ render inertia: "pages/SettingsPage", props: {
21
+ settings: { settings: settings_to_json(@settings) },
22
+ }
23
+ end
24
+
25
+ def update
26
+ @settings = Settings.first_or_create
27
+
28
+ @settings.update(settings_params)
29
+ EasyML::Configuration.configure do |config|
30
+ config.storage = @settings.storage
31
+ config.timezone = @settings.timezone
32
+ config.s3_access_key_id = @settings.s3_access_key_id
33
+ config.s3_secret_access_key = @settings.s3_secret_access_key
34
+ config.s3_bucket = @settings.s3_bucket
35
+ config.s3_region = @settings.s3_region
36
+ config.s3_prefix = @settings.s3_prefix
37
+ end
38
+ flash.now[:notice] = "Settings saved."
39
+ render inertia: "pages/SettingsPage", props: {
40
+ settings: @settings.as_json,
41
+ }
42
+ end
43
+
44
+ private
45
+
46
+ def settings_params
47
+ params.require(:settings).permit(
48
+ :storage,
49
+ :timezone,
50
+ :s3_access_key_id,
51
+ :s3_secret_access_key,
52
+ :s3_bucket,
53
+ :s3_region,
54
+ :s3_prefix,
55
+ :wandb_api_key
56
+ )
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,108 @@
1
+ import React, { createContext, useContext, useState, useCallback } from 'react';
2
+ import { AlertCircle, CheckCircle, XCircle, X } from 'lucide-react';
3
+
4
+ type AlertType = 'success' | 'error' | 'info';
5
+
6
+ interface Alert {
7
+ id: string;
8
+ type: AlertType;
9
+ message: string;
10
+ isLeaving?: boolean;
11
+ }
12
+
13
+ interface AlertContextType {
14
+ alerts: Alert[];
15
+ showAlert: (type: AlertType, message: string) => void;
16
+ removeAlert: (id: string) => void;
17
+ }
18
+
19
+ const AlertContext = createContext<AlertContextType | undefined>(undefined);
20
+
21
+ export function AlertProvider({ children }: { children: React.ReactNode }) {
22
+ const [alerts, setAlerts] = useState<Alert[]>([]);
23
+ let numSeconds = 1.25;
24
+
25
+ const removeAlert = useCallback((id: string) => {
26
+ setAlerts(prev =>
27
+ prev.map(alert =>
28
+ alert.id === id ? { ...alert, isLeaving: true } : alert
29
+ )
30
+ );
31
+
32
+ setTimeout(() => {
33
+ setAlerts(prev => prev.filter(alert => alert.id !== id));
34
+ }, 300);
35
+ }, []);
36
+
37
+ const showAlert = useCallback((type: AlertType, message: string) => {
38
+ const id = Math.random().toString(36).substring(7);
39
+ setAlerts(prev => [...prev, { id, type, message }]);
40
+
41
+ setTimeout(() => {
42
+ removeAlert(id);
43
+ }, numSeconds * 1000);
44
+ }, [removeAlert]);
45
+
46
+ return (
47
+ <AlertContext.Provider value={{ alerts, showAlert, removeAlert }}>
48
+ {children}
49
+ </AlertContext.Provider>
50
+ );
51
+ }
52
+
53
+ export function useAlerts() {
54
+ const context = useContext(AlertContext);
55
+ if (context === undefined) {
56
+ throw new Error('useAlerts must be used within an AlertProvider');
57
+ }
58
+ return context;
59
+ }
60
+
61
+ export function AlertContainer() {
62
+ const { alerts, removeAlert } = useAlerts();
63
+
64
+ if (alerts.length === 0) return null;
65
+
66
+ return (
67
+ <div className="fixed top-4 right-4 left-4 z-50 flex flex-col gap-2">
68
+ {alerts.map(alert => (
69
+ <div
70
+ key={alert.id}
71
+ className={`flex items-center justify-between p-4 rounded-lg shadow-lg
72
+ transition-all duration-300 ease-in-out
73
+ ${alert.isLeaving ? 'opacity-0 feature -translate-y-2' : 'opacity-100'}
74
+ ${
75
+ alert.type === 'success' ? 'bg-green-50 text-green-900' :
76
+ alert.type === 'error' ? 'bg-red-50 text-red-900' :
77
+ 'bg-blue-50 text-blue-900'
78
+ }`}
79
+ >
80
+ <div className="flex items-center gap-3">
81
+ {alert.type === 'success' ? (
82
+ <CheckCircle className={`w-5 h-5 ${
83
+ alert.type === 'success' ? 'text-green-500' :
84
+ alert.type === 'error' ? 'text-red-500' :
85
+ 'text-blue-500'
86
+ }`} />
87
+ ) : alert.type === 'error' ? (
88
+ <XCircle className="w-5 h-5 text-red-500" />
89
+ ) : (
90
+ <AlertCircle className="w-5 h-5 text-blue-500" />
91
+ )}
92
+ <p className="text-sm font-medium">{alert.message}</p>
93
+ </div>
94
+ <button
95
+ onClick={() => removeAlert(alert.id)}
96
+ className={`p-1 rounded-full hover:bg-opacity-10 ${
97
+ alert.type === 'success' ? 'hover:bg-green-900' :
98
+ alert.type === 'error' ? 'hover:bg-red-900' :
99
+ 'hover:bg-blue-900'
100
+ }`}
101
+ >
102
+ <X className="w-4 h-4" />
103
+ </button>
104
+ </div>
105
+ ))}
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,161 @@
1
+ import React, { useState } from 'react';
2
+ import { Database, Table, ChevronDown, ChevronUp, BarChart, ChevronLeft, ChevronRight } from 'lucide-react';
3
+ import type { Dataset, Column } from '../types';
4
+
5
+ interface DatasetPreviewProps {
6
+ dataset: Dataset;
7
+ }
8
+
9
+ const STATS_PER_PAGE = 6;
10
+
11
+ export function DatasetPreview({ dataset }: DatasetPreviewProps) {
12
+ const [showStats, setShowStats] = useState(false);
13
+ const [currentPage, setCurrentPage] = useState(1);
14
+ const columns = dataset.sample_data[0] ? Object.keys(dataset.sample_data[0]) : [];
15
+
16
+ const totalPages = Math.ceil(dataset.columns.length / STATS_PER_PAGE);
17
+ const paginatedColumns = dataset.columns.slice(
18
+ (currentPage - 1) * STATS_PER_PAGE,
19
+ currentPage * STATS_PER_PAGE
20
+ );
21
+
22
+ return (
23
+ <div className="bg-white rounded-lg shadow-lg p-6">
24
+ <div className="flex items-start justify-between mb-6">
25
+ <div>
26
+ <div className="flex items-center gap-2">
27
+ <Database className="w-5 h-5 text-blue-600" />
28
+ <h3 className="text-xl font-semibold text-gray-900">{dataset.name}</h3>
29
+ </div>
30
+ <p className="text-gray-600 mt-1">{dataset.description}</p>
31
+ <p className="text-sm text-gray-500 mt-2">
32
+ {dataset.num_rows.toLocaleString()} rows • <span className="font-medium">Raw:</span> {dataset.columns.length} columns • <span className="font-medium">Processed:</span> {columns.length} columns • Last synced{' '}
33
+ {new Date(dataset.updated_at).toLocaleDateString()}
34
+ </p>
35
+ </div>
36
+ <button
37
+ onClick={() => setShowStats(!showStats)}
38
+ className="flex items-center gap-1 text-blue-600 hover:text-blue-800"
39
+ >
40
+ <BarChart className="w-4 h-4" />
41
+ <span className="text-sm font-medium">
42
+ {showStats ? 'Hide Statistics' : 'Show Statistics'}
43
+ </span>
44
+ {showStats ? (
45
+ <ChevronUp className="w-4 h-4" />
46
+ ) : (
47
+ <ChevronDown className="w-4 h-4" />
48
+ )}
49
+ </button>
50
+ </div>
51
+
52
+ <div className="space-y-6">
53
+ {showStats && (
54
+ <>
55
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
56
+ {paginatedColumns.map((column: Column) => (
57
+ <div
58
+ key={column.name}
59
+ className="bg-gray-50 rounded-lg p-4"
60
+ >
61
+ <div className="flex items-center justify-between mb-2">
62
+ <h4 className="font-medium text-gray-900">{column.name}</h4>
63
+ <span className="text-xs font-medium text-gray-500 px-2 py-1 bg-gray-200 rounded-full">
64
+ {column.datatype}
65
+ </span>
66
+ </div>
67
+ <p className="text-sm text-gray-600 mb-3">{column.description}</p>
68
+ {column.statistics && (
69
+ <div className="space-y-1">
70
+ {Object.entries(column.statistics.raw).map(([key, value]) => {
71
+ if (key === "counts") {
72
+ return null;
73
+ }
74
+ return (
75
+ <div key={key} className="flex justify-between text-sm">
76
+ <span className="text-gray-500">
77
+ {key.charAt(0).toUpperCase() + key.slice(1)}:
78
+ </span>
79
+ <span className="font-medium text-gray-900">
80
+ {typeof value === 'number' ?
81
+ value.toLocaleString(undefined, {
82
+ maximumFractionDigits: 2
83
+ }) :
84
+ value}
85
+ </span>
86
+ </div>
87
+ )})}
88
+ </div>
89
+ )}
90
+ </div>
91
+ ))}
92
+ </div>
93
+
94
+ {totalPages > 1 && (
95
+ <div className="flex items-center justify-between border-t pt-4">
96
+ <div className="text-sm text-gray-500">
97
+ Showing {((currentPage - 1) * STATS_PER_PAGE) + 1} to {Math.min(currentPage * STATS_PER_PAGE, dataset.columns.length)} of {dataset.columns.length} columns
98
+ </div>
99
+ <div className="flex items-center gap-2">
100
+ <button
101
+ onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
102
+ disabled={currentPage === 1}
103
+ className="p-1 rounded-md hover:bg-gray-100 disabled:opacity-50"
104
+ >
105
+ <ChevronLeft className="w-5 h-5" />
106
+ </button>
107
+ <span className="text-sm text-gray-600">
108
+ Page {currentPage} of {totalPages}
109
+ </span>
110
+ <button
111
+ onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
112
+ disabled={currentPage === totalPages}
113
+ className="p-1 rounded-md hover:bg-gray-100 disabled:opacity-50"
114
+ >
115
+ <ChevronRight className="w-5 h-5" />
116
+ </button>
117
+ </div>
118
+ </div>
119
+ )}
120
+ </>
121
+ )}
122
+
123
+ <div className="overflow-x-auto">
124
+ <div className="inline-block min-w-full align-middle">
125
+ <div className="overflow-hidden shadow-sm ring-1 ring-black ring-opacity-5 rounded-lg">
126
+ <table className="min-w-full divide-y divide-gray-300">
127
+ <thead className="bg-gray-50">
128
+ <tr>
129
+ {columns.map((column) => (
130
+ <th
131
+ key={column}
132
+ scope="col"
133
+ className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
134
+ >
135
+ {column}
136
+ </th>
137
+ ))}
138
+ </tr>
139
+ </thead>
140
+ <tbody className="divide-y divide-gray-200 bg-white">
141
+ {dataset.sample_data.map((row: Record<string, any>, i: number) => (
142
+ <tr key={i}>
143
+ {columns.map((column) => (
144
+ <td
145
+ key={row[column]}
146
+ className="whitespace-nowrap px-3 py-4 text-sm text-gray-500"
147
+ >
148
+ {row[column]?.toString()}
149
+ </td>
150
+ ))}
151
+ </tr>
152
+ ))}
153
+ </tbody>
154
+ </table>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ );
161
+ }
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { LucideIcon } from 'lucide-react';
3
+
4
+ interface EmptyStateProps {
5
+ icon: LucideIcon;
6
+ title: string;
7
+ description: string;
8
+ actionLabel: string;
9
+ onAction: () => void;
10
+ }
11
+
12
+ export function EmptyState({ icon: Icon, title, description, actionLabel, onAction }: EmptyStateProps) {
13
+ return (
14
+ <div className="text-center py-12">
15
+ <div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
16
+ <Icon className="w-8 h-8 text-gray-400" />
17
+ </div>
18
+ <h3 className="text-lg font-medium text-gray-900 mb-2">{title}</h3>
19
+ <p className="text-gray-500 mb-6 max-w-sm mx-auto">{description}</p>
20
+ <button
21
+ onClick={onAction}
22
+ className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
23
+ >
24
+ {actionLabel}
25
+ </button>
26
+ </div>
27
+ );
28
+ }