easy_ml 0.1.4 → 0.2.0.pre.rc1

Sign up to get free protection for your applications and to get access to all the features.
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
+ }