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,147 @@
1
+ import React from 'react';
2
+ import { router, usePage } from '@inertiajs/react';
3
+ import { useInertiaForm } from 'use-inertia-form';
4
+ import { SearchableSelect } from '../components/SearchableSelect';
5
+ import type { Datasource, DatasourceFormProps } from '../types/datasource';
6
+
7
+ export default function DatasourceFormPage({ datasource, constants }: DatasourceFormProps) {
8
+ const { rootPath } = usePage().props;
9
+ const isEditing = !!datasource;
10
+
11
+ const { data, setData, processing, errors } = useInertiaForm<{ datasource: Datasource }>({
12
+ datasource: {
13
+ name: datasource?.name ?? '',
14
+ datasource_type: datasource?.datasource_type ?? 's3',
15
+ s3_bucket: datasource?.s3_bucket ?? '',
16
+ s3_prefix: datasource?.s3_prefix ?? '',
17
+ s3_region: datasource?.s3_region ?? 'us-east-1',
18
+ }
19
+ });
20
+
21
+ const handleSubmit = (e: React.FormEvent) => {
22
+ e.preventDefault();
23
+ if (isEditing) {
24
+ router.patch(`${rootPath}/datasources/${datasource.id}`, data);
25
+ } else {
26
+ router.post(`${rootPath}/datasources`, data);
27
+ }
28
+ };
29
+
30
+ return (
31
+ <div className="max-w-2xl mx-auto py-8">
32
+ <div className="bg-white rounded-lg shadow-lg p-6">
33
+ <h2 className="text-xl font-semibold text-gray-900 mb-6">
34
+ {isEditing ? 'Edit Datasource' : 'New Datasource'}
35
+ </h2>
36
+
37
+ <form onSubmit={handleSubmit} className="space-y-6">
38
+ <div>
39
+ <label
40
+ htmlFor="name"
41
+ className="block text-sm font-medium text-gray-700"
42
+ >
43
+ Name
44
+ </label>
45
+ <input
46
+ type="text"
47
+ id="name"
48
+ value={data.datasource.name}
49
+ onChange={(e) => setData('datasource.name', e.target.value)}
50
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-2 px-4 shadow-sm border-gray-300 border"
51
+ required
52
+ />
53
+ {errors.datasource?.name && (
54
+ <p className="mt-1 text-sm text-red-600">{errors.datasource.name}</p>
55
+ )}
56
+ </div>
57
+
58
+ {!isEditing && (
59
+ <div>
60
+ <label className="block text-sm font-medium text-gray-700 mb-1">
61
+ Type
62
+ </label>
63
+ <SearchableSelect
64
+ options={constants.DATASOURCE_TYPES}
65
+ value={data.datasource.datasource_type}
66
+ onChange={(value) => setData('datasource.datasource_type', value)}
67
+ placeholder="Select datasource type"
68
+ />
69
+ </div>
70
+ )}
71
+
72
+ <div>
73
+ <label
74
+ htmlFor="s3_bucket"
75
+ className="block text-sm font-medium text-gray-700"
76
+ >
77
+ S3 Bucket
78
+ </label>
79
+ <input
80
+ type="text"
81
+ id="s3_bucket"
82
+ value={data.datasource.s3_bucket}
83
+ onChange={(e) => setData('datasource.s3_bucket', e.target.value)}
84
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-2 px-4 shadow-sm border-gray-300 border"
85
+ required
86
+ />
87
+ {errors.datasource?.s3_bucket && (
88
+ <p className="mt-1 text-sm text-red-600">{errors.datasource.s3_bucket}</p>
89
+ )}
90
+ </div>
91
+
92
+ <div>
93
+ <label
94
+ htmlFor="s3_prefix"
95
+ className="block text-sm font-medium text-gray-700"
96
+ >
97
+ S3 Prefix
98
+ </label>
99
+ <input
100
+ type="text"
101
+ id="s3_prefix"
102
+ value={data.datasource.s3_prefix}
103
+ onChange={(e) => setData('datasource.s3_prefix', e.target.value)}
104
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-2 px-4 shadow-sm border-gray-300 border"
105
+ placeholder="data/raw/"
106
+ />
107
+ {errors.datasource?.s3_prefix && (
108
+ <p className="mt-1 text-sm text-red-600">{errors.datasource.s3_prefix}</p>
109
+ )}
110
+ </div>
111
+
112
+ <div>
113
+ <label className="block text-sm font-medium text-gray-700 mb-1">
114
+ S3 Region
115
+ </label>
116
+ <SearchableSelect
117
+ options={constants.s3.S3_REGIONS}
118
+ value={data.datasource.s3_region}
119
+ onChange={(value) => setData('datasource.s3_region', value)}
120
+ placeholder="Select s3 region"
121
+ />
122
+ {errors.datasource?.s3_region && (
123
+ <p className="mt-1 text-sm text-red-600">{errors.datasource.s3_region}</p>
124
+ )}
125
+ </div>
126
+
127
+ <div className="flex justify-end gap-3">
128
+ <button
129
+ type="button"
130
+ onClick={() => router.visit(`${rootPath}/datasources`)}
131
+ className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900"
132
+ >
133
+ Cancel
134
+ </button>
135
+ <button
136
+ type="submit"
137
+ disabled={processing}
138
+ className="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"
139
+ >
140
+ {processing ? 'Saving...' : isEditing ? 'Save Changes' : 'Create Datasource'}
141
+ </button>
142
+ </div>
143
+ </form>
144
+ </div>
145
+ </div>
146
+ );
147
+ }
@@ -0,0 +1,261 @@
1
+ import React, { useState, useMemo, useEffect } from 'react';
2
+ import { Link, usePage, router } from '@inertiajs/react';
3
+ import { HardDrive, Plus, Trash2, Settings, RefreshCw, ChevronDown, ChevronUp, AlertCircle } from 'lucide-react';
4
+ import { EmptyState } from '../components/EmptyState';
5
+ import { SearchInput } from '../components/SearchInput';
6
+ import { Pagination } from '../components/Pagination';
7
+ import type { Datasource } from '../types/datasource';
8
+ import { Badge } from "@/components/ui/badge";
9
+
10
+ const ITEMS_PER_PAGE = 6;
11
+
12
+ export default function DatasourcesPage({ datasources }: { datasources: Datasource[] }) {
13
+ const { rootPath } = usePage().props;
14
+ const [searchQuery, setSearchQuery] = useState('');
15
+ const [currentPage, setCurrentPage] = useState(1);
16
+ const [expandedErrors, setExpandedErrors] = useState<number[]>([]);
17
+
18
+ const filteredDatasources = useMemo(() => {
19
+ return datasources.filter(source =>
20
+ source.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
21
+ source.s3_bucket.toLowerCase().includes(searchQuery.toLowerCase())
22
+ );
23
+ }, [searchQuery, datasources]);
24
+
25
+ const totalPages = Math.ceil(filteredDatasources.length / ITEMS_PER_PAGE);
26
+ const paginatedDatasources = filteredDatasources.slice(
27
+ (currentPage - 1) * ITEMS_PER_PAGE,
28
+ currentPage * ITEMS_PER_PAGE
29
+ );
30
+
31
+ const handleDelete = (datasourceId: number) => {
32
+ if (confirm('Are you sure you want to delete this datasource? This action cannot be undone.')) {
33
+ router.delete(`${rootPath}/datasources/${datasourceId}`);
34
+ }
35
+ };
36
+
37
+ const toggleError = (id: number) => {
38
+ setExpandedErrors(prev =>
39
+ prev.includes(id)
40
+ ? prev.filter(expandedId => expandedId !== id)
41
+ : [...prev, id]
42
+ );
43
+ };
44
+
45
+ const handleSync = async (id: number) => {
46
+ try {
47
+ router.post(`${rootPath}/datasources/${id}/sync`, {}, {
48
+ preserveScroll: true, // Keeps the scroll position
49
+ preserveState: true, // Keeps the form state
50
+ onSuccess: () => {
51
+ // The page will automatically refresh with new data
52
+ },
53
+ onError: () => {
54
+ // Handle error case if needed
55
+ console.error('Failed to sync datasource');
56
+ }
57
+ });
58
+ } catch (error) {
59
+ console.error('Failed to sync datasource:', error);
60
+ }
61
+ };
62
+
63
+ const formatLastSyncedAt = (lastSyncedAt: string) => {
64
+ if (lastSyncedAt === 'Not Synced') return lastSyncedAt;
65
+
66
+ const date = new Date(lastSyncedAt);
67
+ return isNaN(date.getTime()) ? lastSyncedAt : date.toLocaleString();
68
+ };
69
+
70
+ useEffect(() => {
71
+ let pollInterval: number | undefined;
72
+
73
+ // Check if any datasource is syncing
74
+ const isAnySyncing = datasources.some(d => d.is_syncing);
75
+
76
+ if (isAnySyncing) {
77
+ // Start polling every 5 seconds
78
+ pollInterval = window.setInterval(() => {
79
+ router.get(window.location.href, {}, {
80
+ preserveScroll: true,
81
+ preserveState: true,
82
+ only: ['datasources']
83
+ });
84
+ }, 2000);
85
+ }
86
+
87
+ // Cleanup function
88
+ return () => {
89
+ if (pollInterval) {
90
+ window.clearInterval(pollInterval);
91
+ }
92
+ };
93
+ }, [datasources]);
94
+
95
+ if (datasources.length === 0) {
96
+ return (
97
+ <div className="p-8">
98
+ <EmptyState
99
+ icon={HardDrive}
100
+ title="Connect your first data source"
101
+ description="Connect to your data sources to start creating datasets and training models"
102
+ actionLabel="Add Datasource"
103
+ onAction={() => { router.visit(`${rootPath}/datasources/new`) }}
104
+ />
105
+ </div>
106
+ );
107
+ }
108
+
109
+ return (
110
+ <div className="p-8">
111
+ <div className="space-y-6">
112
+ <div className="flex justify-between items-center">
113
+ <div className="flex items-center gap-4">
114
+ <h2 className="text-xl font-semibold text-gray-900">Datasources</h2>
115
+ <SearchInput
116
+ value={searchQuery}
117
+ onChange={setSearchQuery}
118
+ placeholder="Search datasources..."
119
+ />
120
+ </div>
121
+ <Link
122
+ href={`${rootPath}/datasources/new`}
123
+ 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"
124
+ >
125
+ <Plus className="w-4 h-4" />
126
+ New Datasource
127
+ </Link>
128
+ </div>
129
+
130
+ {paginatedDatasources.length === 0 ? (
131
+ <div className="text-center py-12 bg-white rounded-lg shadow">
132
+ <HardDrive className="mx-auto h-12 w-12 text-gray-400" />
133
+ <h3 className="mt-2 text-sm font-medium text-gray-900">No datasources found</h3>
134
+ <p className="mt-1 text-sm text-gray-500">
135
+ No datasources match your search criteria. Try adjusting your search or add a new datasource.
136
+ </p>
137
+ <div className="mt-6">
138
+ <Link
139
+ href={`${rootPath}/datasources/new`}
140
+ className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
141
+ >
142
+ <Plus className="w-4 h-4 mr-2" />
143
+ New Datasource
144
+ </Link>
145
+ </div>
146
+ </div>
147
+ ) : (
148
+ <>
149
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
150
+ {paginatedDatasources.map((datasource) => (
151
+ <div
152
+ key={datasource.id}
153
+ className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow"
154
+ >
155
+ <div className="flex justify-between items-start mb-4">
156
+ <div className="flex items-start gap-3">
157
+ <HardDrive className="w-5 h-5 text-blue-600 mt-1" />
158
+ <div>
159
+ <div className="flex items-center gap-2">
160
+ <h3 className="text-lg font-semibold text-gray-900">
161
+ {datasource.name}
162
+ </h3>
163
+ {datasource.is_syncing ? (
164
+ <Badge variant="warning">syncing</Badge>
165
+ ) : datasource.sync_error ? (
166
+ <Badge variant="important">sync error</Badge>
167
+ ) : datasource.last_synced_at !== 'Not Synced' ? (
168
+ <Badge variant="success">synced</Badge>
169
+ ) : (
170
+ <Badge variant="warning">not synced</Badge>
171
+ )}
172
+ </div>
173
+ <p className="text-sm text-gray-500 mt-1">
174
+ s3://{datasource.s3_bucket}/{datasource.s3_prefix}
175
+ </p>
176
+ </div>
177
+ </div>
178
+ <div className="flex gap-2">
179
+ <button
180
+ onClick={() => handleSync(datasource.id)}
181
+ disabled={datasource.is_syncing}
182
+ className={`text-gray-400 hover:text-blue-600 transition-colors ${
183
+ datasource.is_syncing ? 'animate-spin' : ''
184
+ }`}
185
+ title="Sync datasource"
186
+ >
187
+ <RefreshCw className="w-5 h-5" />
188
+ </button>
189
+ <Link
190
+ href={`${rootPath}/datasources/${datasource.id}/edit`}
191
+ className="text-gray-400 hover:text-blue-600 transition-colors"
192
+ title="Edit datasource"
193
+ >
194
+ <Settings className="w-5 h-5" />
195
+ </Link>
196
+ <button
197
+ onClick={() => handleDelete(datasource.id)}
198
+ className="text-gray-400 hover:text-red-600 transition-colors"
199
+ title="Delete datasource"
200
+ >
201
+ <Trash2 className="w-5 h-5" />
202
+ </button>
203
+ </div>
204
+ </div>
205
+
206
+ <div className="grid grid-cols-2 gap-4 mt-4">
207
+ <div>
208
+ <span className="text-sm text-gray-500">Region</span>
209
+ <p className="text-sm font-medium text-gray-900">
210
+ {datasource.s3_region}
211
+ </p>
212
+ </div>
213
+ <div>
214
+ <span className="text-sm text-gray-500">Last Sync</span>
215
+ <p className="text-sm font-medium text-gray-900">
216
+ {formatLastSyncedAt(datasource.last_synced_at)}
217
+ </p>
218
+ </div>
219
+ </div>
220
+
221
+ {datasource.sync_error && datasource.stacktrace && (
222
+ <div className="mt-4 pt-4 border-t border-gray-100">
223
+ <button
224
+ onClick={() => toggleError(datasource.id)}
225
+ className="flex items-center gap-2 text-sm text-red-600 hover:text-red-700"
226
+ >
227
+ <AlertCircle className="w-4 h-4" />
228
+ <span>View Error Details</span>
229
+ {expandedErrors.includes(datasource.id) ? (
230
+ <ChevronUp className="w-4 h-4" />
231
+ ) : (
232
+ <ChevronDown className="w-4 h-4" />
233
+ )}
234
+ </button>
235
+ {expandedErrors.includes(datasource.id) && (
236
+ <div className="mt-2 p-3 bg-red-50 rounded-md">
237
+ <pre className="text-xs text-red-700 whitespace-pre-wrap font-mono">
238
+ {datasource.stacktrace}
239
+ </pre>
240
+ </div>
241
+ )}
242
+ </div>
243
+ )}
244
+
245
+ </div>
246
+ ))}
247
+ </div>
248
+
249
+ {totalPages > 1 && (
250
+ <Pagination
251
+ currentPage={currentPage}
252
+ totalPages={totalPages}
253
+ onPageChange={setCurrentPage}
254
+ />
255
+ )}
256
+ </>
257
+ )}
258
+ </div>
259
+ </div>
260
+ );
261
+ }
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { router, Link, usePage } from "@inertiajs/react";
3
+ import { useInertiaForm } from "use-inertia-form";
4
+ import { ArrowLeft, Brain } from 'lucide-react';
5
+ import { ModelForm } from '../components/ModelForm';
6
+ import { Model, Dataset } from '../types';
7
+
8
+ interface ModelFormData {
9
+ model: Model;
10
+ }
11
+
12
+ interface PageProps {
13
+ model: Model;
14
+ datasets: Dataset[];
15
+ constants: {
16
+ modelTypes: string[];
17
+ tasks: string[];
18
+ objectives: string[];
19
+ metrics: string[];
20
+ };
21
+ }
22
+
23
+ export default function EditModelPage({ model, datasets, constants }: PageProps) {
24
+ return (
25
+ <div className="max-w-3xl mx-auto py-8">
26
+ <div className="bg-white rounded-lg shadow-lg">
27
+ <div className="px-6 py-4 border-b border-gray-200">
28
+ <div className="flex items-center gap-3">
29
+ <Brain className="w-6 h-6 text-blue-600" />
30
+ <h2 className="text-xl font-semibold text-gray-900">Edit Model</h2>
31
+ </div>
32
+ </div>
33
+
34
+ <div className="p-6">
35
+ <ModelForm
36
+ initialData={model}
37
+ datasets={datasets}
38
+ constants={constants}
39
+ isEditing={true}
40
+ />
41
+ </div>
42
+ </div>
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+ // import { useNavigate, useParams } from 'react-router-dom';
3
+ import { Code2 } from 'lucide-react';
4
+ import { mockDatasets, mockFeatureGroups } from '../mockData';
5
+ import { FeatureForm } from '../components/features/FeatureForm';
6
+
7
+ export default function EditFeaturePage() {
8
+ const navigate = useNavigate();
9
+ const { id } = useParams();
10
+
11
+ const feature = mockFeatureGroups
12
+ .flatMap(g => g.features)
13
+ .find(t => t.id === Number(id));
14
+
15
+ if (!feature) {
16
+ return (
17
+ <div className="text-center py-12">
18
+ <h2 className="text-xl font-semibold text-gray-900">Feature not found</h2>
19
+ </div>
20
+ );
21
+ }
22
+
23
+ const handleSubmit = (data: any) => {
24
+ console.log('Updating feature:', data);
25
+ navigate('/features');
26
+ };
27
+
28
+ return (
29
+ <div className="max-w-4xl mx-auto p-8">
30
+ <div className="bg-white rounded-lg shadow-lg">
31
+ <div className="px-6 py-4 border-b border-gray-200">
32
+ <div className="flex items-center gap-3">
33
+ <Code2 className="w-6 h-6 text-blue-600" />
34
+ <h2 className="text-xl font-semibold text-gray-900">Edit Feature</h2>
35
+ </div>
36
+ </div>
37
+
38
+ <FeatureForm
39
+ datasets={mockDatasets}
40
+ groups={mockFeatureGroups}
41
+ initialData={{
42
+ name: feature.name,
43
+ description: feature.description,
44
+ groupId: feature.groupId,
45
+ testDatasetId: feature.testDatasetId,
46
+ inputColumns: feature.inputColumns,
47
+ outputColumns: feature.outputColumns,
48
+ code: feature.code
49
+ }}
50
+ onSubmit={handleSubmit}
51
+ onCancel={() => navigate('/features')}
52
+ />
53
+ </div>
54
+ </div>
55
+ );
56
+ }
@@ -0,0 +1,115 @@
1
+ import React, { useState, useMemo, useEffect } from 'react';
2
+ import { Brain, Plus, Trash2 } from 'lucide-react';
3
+ import { ModelCard } from '../components/ModelCard';
4
+ import { EmptyState } from '../components/EmptyState';
5
+ import { SearchInput } from '../components/SearchInput';
6
+ import { Pagination } from '../components/Pagination';
7
+ import { router } from '@inertiajs/react';
8
+
9
+ const ITEMS_PER_PAGE = 6;
10
+
11
+ export default function ModelsPage({ rootPath, models }) {
12
+ const [selectedModelId, setSelectedModelId] = useState<number | null>(null);
13
+ const [searchQuery, setSearchQuery] = useState('');
14
+ const [currentPage, setCurrentPage] = useState(1);
15
+
16
+ const filteredModels = useMemo(() => {
17
+ return models.filter(model =>
18
+ model.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
19
+ model.model_type.toLowerCase().includes(searchQuery.toLowerCase())
20
+ );
21
+ }, [searchQuery, models]);
22
+
23
+ const totalPages = Math.ceil(filteredModels.length / ITEMS_PER_PAGE);
24
+ const paginatedModels = filteredModels.slice(
25
+ (currentPage - 1) * ITEMS_PER_PAGE,
26
+ currentPage * ITEMS_PER_PAGE
27
+ );
28
+
29
+ const handleDelete = (modelId: number) => {
30
+ if (confirm('Are you sure you want to delete this model?')) {
31
+ router.delete(`${rootPath}/models/${modelId}`);
32
+ }
33
+ };
34
+
35
+ if (models.length === 0) {
36
+ return (
37
+ <div className="p-8">
38
+ <EmptyState
39
+ icon={Brain}
40
+ title="Create your first ML model"
41
+ description="Get started by creating a machine learning model. You can train models for classification, regression, and more."
42
+ actionLabel="Create Model"
43
+ onAction={() => {
44
+ router.visit(`${rootPath}/models/new`)
45
+ }}
46
+ />
47
+ </div>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <div className="p-8">
53
+ <div className="space-y-6">
54
+ <div className="flex justify-between items-center">
55
+ <div className="flex items-center gap-4">
56
+ <h2 className="text-xl font-semibold text-gray-900">Models</h2>
57
+ <SearchInput
58
+ value={searchQuery}
59
+ onChange={setSearchQuery}
60
+ placeholder="Search models..."
61
+ />
62
+ </div>
63
+ <button
64
+ onClick={() => router.visit(`${rootPath}/models/new`)}
65
+ 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"
66
+ >
67
+ <Plus className="w-4 h-4" />
68
+ New Model
69
+ </button>
70
+ </div>
71
+
72
+ {paginatedModels.length === 0 ? (
73
+ <div className="text-center py-12 bg-white rounded-lg shadow">
74
+ <Brain className="mx-auto h-12 w-12 text-gray-400" />
75
+ <h3 className="mt-2 text-sm font-medium text-gray-900">No models found</h3>
76
+ <p className="mt-1 text-sm text-gray-500">
77
+ No models match your search criteria. Try adjusting your search or create a new model.
78
+ </p>
79
+ <div className="mt-6">
80
+ <button
81
+ onClick={() => router.visit(`${rootPath}/models/new`)}
82
+ className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
83
+ >
84
+ <Plus className="w-4 h-4 mr-2" />
85
+ New Model
86
+ </button>
87
+ </div>
88
+ </div>
89
+ ) : (
90
+ <>
91
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
92
+ {paginatedModels.map((model) => (
93
+ <ModelCard
94
+ rootPath={rootPath}
95
+ key={model.id}
96
+ initialModel={model}
97
+ onViewDetails={setSelectedModelId}
98
+ handleDelete={handleDelete}
99
+ />
100
+ ))}
101
+ </div>
102
+
103
+ {totalPages > 1 && (
104
+ <Pagination
105
+ currentPage={currentPage}
106
+ totalPages={totalPages}
107
+ onPageChange={setCurrentPage}
108
+ />
109
+ )}
110
+ </>
111
+ )}
112
+ </div>
113
+ </div>
114
+ );
115
+ }