easy_ml 0.2.0.pre.rc72 → 0.2.0.pre.rc75

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/easy_ml/datasets_controller.rb +33 -0
  3. data/app/controllers/easy_ml/datasources_controller.rb +7 -0
  4. data/app/controllers/easy_ml/models_controller.rb +38 -0
  5. data/app/frontend/components/DatasetCard.tsx +212 -0
  6. data/app/frontend/components/ModelCard.tsx +69 -29
  7. data/app/frontend/components/StackTrace.tsx +13 -0
  8. data/app/frontend/components/dataset/FeatureConfigPopover.tsx +10 -7
  9. data/app/frontend/components/datasets/UploadDatasetButton.tsx +51 -0
  10. data/app/frontend/components/models/DownloadModelModal.tsx +90 -0
  11. data/app/frontend/components/models/UploadModelModal.tsx +212 -0
  12. data/app/frontend/components/models/index.ts +2 -0
  13. data/app/frontend/pages/DatasetsPage.tsx +36 -130
  14. data/app/frontend/pages/DatasourcesPage.tsx +22 -2
  15. data/app/frontend/pages/ModelsPage.tsx +37 -11
  16. data/app/frontend/types/dataset.ts +1 -2
  17. data/app/frontend/types.ts +1 -1
  18. data/app/jobs/easy_ml/training_job.rb +2 -2
  19. data/app/models/easy_ml/column/imputers/base.rb +4 -0
  20. data/app/models/easy_ml/column/imputers/clip.rb +5 -3
  21. data/app/models/easy_ml/column/imputers/imputer.rb +11 -13
  22. data/app/models/easy_ml/column/imputers/mean.rb +7 -3
  23. data/app/models/easy_ml/column/imputers/null_imputer.rb +3 -0
  24. data/app/models/easy_ml/column/imputers/ordinal_encoder.rb +5 -1
  25. data/app/models/easy_ml/column/imputers.rb +3 -1
  26. data/app/models/easy_ml/column/lineage/base.rb +5 -1
  27. data/app/models/easy_ml/column/lineage/computed_by_feature.rb +1 -1
  28. data/app/models/easy_ml/column/lineage/preprocessed.rb +1 -1
  29. data/app/models/easy_ml/column/lineage/raw_dataset.rb +1 -1
  30. data/app/models/easy_ml/column/selector.rb +4 -0
  31. data/app/models/easy_ml/column.rb +79 -63
  32. data/app/models/easy_ml/column_history.rb +28 -28
  33. data/app/models/easy_ml/column_list/imputer.rb +23 -0
  34. data/app/models/easy_ml/column_list.rb +39 -26
  35. data/app/models/easy_ml/dataset/learner/base.rb +34 -0
  36. data/app/models/easy_ml/dataset/learner/eager/boolean.rb +10 -0
  37. data/app/models/easy_ml/dataset/learner/eager/categorical.rb +51 -0
  38. data/app/models/easy_ml/dataset/learner/eager/query.rb +37 -0
  39. data/app/models/easy_ml/dataset/learner/eager.rb +43 -0
  40. data/app/models/easy_ml/dataset/learner/lazy/boolean.rb +13 -0
  41. data/app/models/easy_ml/dataset/learner/lazy/categorical.rb +10 -0
  42. data/app/models/easy_ml/dataset/learner/lazy/datetime.rb +19 -0
  43. data/app/models/easy_ml/dataset/learner/lazy/null.rb +17 -0
  44. data/app/models/easy_ml/dataset/learner/lazy/numeric.rb +19 -0
  45. data/app/models/easy_ml/dataset/learner/lazy/query.rb +69 -0
  46. data/app/models/easy_ml/dataset/learner/lazy/string.rb +19 -0
  47. data/app/models/easy_ml/dataset/learner/lazy.rb +51 -0
  48. data/app/models/easy_ml/dataset/learner/query.rb +25 -0
  49. data/app/models/easy_ml/dataset/learner.rb +100 -0
  50. data/app/models/easy_ml/dataset.rb +150 -36
  51. data/app/models/easy_ml/dataset_history.rb +1 -0
  52. data/app/models/easy_ml/datasource.rb +9 -0
  53. data/app/models/easy_ml/event.rb +4 -0
  54. data/app/models/easy_ml/export/column.rb +27 -0
  55. data/app/models/easy_ml/export/dataset.rb +37 -0
  56. data/app/models/easy_ml/export/datasource.rb +12 -0
  57. data/app/models/easy_ml/export/feature.rb +24 -0
  58. data/app/models/easy_ml/export/model.rb +40 -0
  59. data/app/models/easy_ml/export/retraining_job.rb +20 -0
  60. data/app/models/easy_ml/export/splitter.rb +14 -0
  61. data/app/models/easy_ml/feature.rb +21 -0
  62. data/app/models/easy_ml/import/column.rb +35 -0
  63. data/app/models/easy_ml/import/dataset.rb +148 -0
  64. data/app/models/easy_ml/import/feature.rb +36 -0
  65. data/app/models/easy_ml/import/model.rb +136 -0
  66. data/app/models/easy_ml/import/retraining_job.rb +29 -0
  67. data/app/models/easy_ml/import/splitter.rb +34 -0
  68. data/app/models/easy_ml/lineage.rb +44 -0
  69. data/app/models/easy_ml/model.rb +93 -36
  70. data/app/models/easy_ml/model_file.rb +6 -0
  71. data/app/models/easy_ml/models/xgboost/evals_callback.rb +7 -7
  72. data/app/models/easy_ml/models/xgboost.rb +33 -9
  73. data/app/models/easy_ml/retraining_job.rb +8 -1
  74. data/app/models/easy_ml/retraining_run.rb +6 -4
  75. data/app/models/easy_ml/splitter.rb +8 -0
  76. data/app/models/lineage_history.rb +6 -0
  77. data/app/serializers/easy_ml/column_serializer.rb +7 -1
  78. data/app/serializers/easy_ml/dataset_serializer.rb +2 -1
  79. data/app/serializers/easy_ml/lineage_serializer.rb +9 -0
  80. data/config/routes.rb +13 -1
  81. data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +3 -3
  82. data/lib/easy_ml/core/tuner.rb +12 -11
  83. data/lib/easy_ml/data/polars_column.rb +149 -100
  84. data/lib/easy_ml/data/polars_reader.rb +8 -5
  85. data/lib/easy_ml/data/polars_schema.rb +56 -0
  86. data/lib/easy_ml/data/splits/file_split.rb +20 -2
  87. data/lib/easy_ml/data/splits/split.rb +10 -1
  88. data/lib/easy_ml/data.rb +1 -0
  89. data/lib/easy_ml/deep_compact.rb +19 -0
  90. data/lib/easy_ml/feature_store.rb +2 -6
  91. data/lib/easy_ml/railtie/generators/migration/migration_generator.rb +6 -0
  92. data/lib/easy_ml/railtie/templates/migration/add_extra_metadata_to_columns.rb.tt +9 -0
  93. data/lib/easy_ml/railtie/templates/migration/add_raw_schema_to_datasets.rb.tt +9 -0
  94. data/lib/easy_ml/railtie/templates/migration/add_unique_constraint_to_easy_ml_model_names.rb.tt +8 -0
  95. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_lineages.rb.tt +24 -0
  96. data/lib/easy_ml/railtie/templates/migration/remove_evaluator_from_retraining_jobs.rb.tt +7 -0
  97. data/lib/easy_ml/railtie/templates/migration/update_preprocessing_steps_to_jsonb.rb.tt +18 -0
  98. data/lib/easy_ml/timing.rb +34 -0
  99. data/lib/easy_ml/version.rb +1 -1
  100. data/lib/easy_ml.rb +2 -0
  101. data/public/easy_ml/assets/.vite/manifest.json +2 -2
  102. data/public/easy_ml/assets/assets/Application-Q7L6ioxr.css +1 -0
  103. data/public/easy_ml/assets/assets/entrypoints/Application.tsx-Rrzo4ecT.js +522 -0
  104. data/public/easy_ml/assets/assets/entrypoints/Application.tsx-Rrzo4ecT.js.map +1 -0
  105. metadata +52 -12
  106. data/app/models/easy_ml/column/learners/base.rb +0 -103
  107. data/app/models/easy_ml/column/learners/boolean.rb +0 -11
  108. data/app/models/easy_ml/column/learners/categorical.rb +0 -51
  109. data/app/models/easy_ml/column/learners/datetime.rb +0 -19
  110. data/app/models/easy_ml/column/learners/null.rb +0 -22
  111. data/app/models/easy_ml/column/learners/numeric.rb +0 -33
  112. data/app/models/easy_ml/column/learners/string.rb +0 -15
  113. data/public/easy_ml/assets/assets/Application-B3sRjyMT.css +0 -1
  114. data/public/easy_ml/assets/assets/entrypoints/Application.tsx-Dfg-nTrB.js +0 -489
  115. data/public/easy_ml/assets/assets/entrypoints/Application.tsx-Dfg-nTrB.js.map +0 -1
@@ -0,0 +1,212 @@
1
+ import React, { useState, useRef } from 'react';
2
+ import { FileUp, FileJson, Database, Upload, ArrowRight } from 'lucide-react';
3
+ import { usePage, router } from '@inertiajs/react';
4
+ import { useInertiaForm } from 'use-inertia-form';
5
+ import { useDropzone } from 'react-dropzone';
6
+ import { SearchableSelect } from '../SearchableSelect';
7
+
8
+ interface UploadModelModalProps {
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ modelId?: number;
12
+ dataset_id?: number;
13
+ }
14
+
15
+ interface UploadForm {
16
+ config: File | null;
17
+ dataset_id: string;
18
+ }
19
+
20
+ interface PageProps {
21
+ rootPath: string;
22
+ datasets: Pick<Dataset, 'id' | 'name' | 'num_rows'>[];
23
+ }
24
+
25
+ export function UploadModelModal({ isOpen, onClose, modelId, dataset_id }: UploadModelModalProps) {
26
+ const { rootPath, datasets } = usePage<PageProps>().props;
27
+ const [selectedOption, setSelectedOption] = useState<'model' | 'both' | null>(dataset_id ? 'model' : null);
28
+
29
+ const { data, setData, post, processing, errors } = useInertiaForm<UploadForm>({
30
+ config: null,
31
+ dataset_id: dataset_id ? dataset_id.toString() : '',
32
+ });
33
+
34
+ const onDrop = (acceptedFiles: File[]) => {
35
+ const file = acceptedFiles[0];
36
+ if (file) {
37
+ setData('config', file);
38
+ }
39
+ };
40
+
41
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
42
+ onDrop,
43
+ accept: {
44
+ 'application/json': ['.json']
45
+ },
46
+ multiple: false
47
+ });
48
+
49
+ const canUpload = data.config && (dataset_id || (selectedOption && (
50
+ selectedOption === 'both' || (selectedOption === 'model' && data.dataset_id)
51
+ )));
52
+
53
+ const handleUpload = () => {
54
+ if (!canUpload) return;
55
+
56
+ const formData = new FormData();
57
+ if (data.config) {
58
+ formData.append('config', data.config);
59
+ }
60
+
61
+ // When dataset_id prop is present, we're in model-only mode
62
+ if (dataset_id) {
63
+ formData.append('include_dataset', 'false');
64
+ formData.append('dataset_id', dataset_id.toString());
65
+ } else {
66
+ // Otherwise, use the selected option and dataset_id from form
67
+ formData.append('include_dataset', (selectedOption === 'both').toString());
68
+ if (selectedOption === 'model' && data.dataset_id) {
69
+ formData.append('dataset_id', data.dataset_id);
70
+ }
71
+ }
72
+
73
+ // Close modal immediately
74
+ onClose();
75
+
76
+ // Determine the correct endpoint based on whether we're updating or creating
77
+ const endpoint = modelId
78
+ ? `${rootPath}/models/${modelId}/upload`
79
+ : `${rootPath}/models/upload`;
80
+
81
+ // Post the data and handle the response
82
+ post(endpoint, formData, {
83
+ preserveScroll: false,
84
+ onSuccess: () => {
85
+ // Force a full page refresh to get the latest data
86
+ window.location.href = window.location.href;
87
+ },
88
+ onError: () => {
89
+ // If there's an error, reopen the modal to show the error state
90
+ onClose();
91
+ }
92
+ });
93
+ };
94
+
95
+ if (!isOpen) return null;
96
+
97
+ return (
98
+ <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50">
99
+ <div className="bg-white rounded-xl p-6 w-full max-w-md shadow-2xl">
100
+ <div className="flex items-center justify-between mb-6">
101
+ <h3 className="text-lg font-semibold text-gray-900">Upload Configuration</h3>
102
+ <div className="p-2 bg-blue-50 rounded-lg">
103
+ <FileUp className="w-5 h-5 text-blue-600" />
104
+ </div>
105
+ </div>
106
+
107
+ {!dataset_id && (
108
+ <div className="space-y-3">
109
+ <button
110
+ onClick={() => {
111
+ setSelectedOption('model');
112
+ setData('dataset_id', '');
113
+ }}
114
+ className={`w-full px-4 py-3 rounded-lg text-left transition-all duration-200 ${
115
+ selectedOption === 'model'
116
+ ? 'bg-blue-50 border-2 border-blue-500 ring-2 ring-blue-200'
117
+ : 'bg-white border-2 border-gray-200 hover:border-blue-200'
118
+ }`}
119
+ >
120
+ <div className="flex items-center justify-between">
121
+ <div>
122
+ <div className="font-medium text-gray-900">Model Only</div>
123
+ <div className="text-sm text-gray-500">Upload model configuration and select a dataset</div>
124
+ </div>
125
+ <FileJson className={`w-5 h-5 ${selectedOption === 'model' ? 'text-blue-600' : 'text-gray-400'}`} />
126
+ </div>
127
+ </button>
128
+
129
+ <button
130
+ onClick={() => {
131
+ setSelectedOption('both');
132
+ setData('dataset_id', '');
133
+ }}
134
+ className={`w-full px-4 py-3 rounded-lg text-left transition-all duration-200 ${
135
+ selectedOption === 'both'
136
+ ? 'bg-blue-50 border-2 border-blue-500 ring-2 ring-blue-200'
137
+ : 'bg-white border-2 border-gray-200 hover:border-blue-200'
138
+ }`}
139
+ >
140
+ <div className="flex items-center justify-between">
141
+ <div>
142
+ <div className="font-medium text-gray-900">Model + Dataset</div>
143
+ <div className="text-sm text-gray-500">Upload and validate both model and dataset configurations</div>
144
+ </div>
145
+ <Database className={`w-5 h-5 ${selectedOption === 'both' ? 'text-blue-600' : 'text-gray-400'}`} />
146
+ </div>
147
+ </button>
148
+ </div>
149
+ )}
150
+
151
+ {selectedOption === 'model' && !dataset_id && (
152
+ <div className="mt-4">
153
+ <label className="block text-sm font-medium text-gray-700 mb-1">
154
+ Select Dataset
155
+ </label>
156
+ <SearchableSelect
157
+ options={datasets.map(dataset => ({
158
+ value: dataset.id,
159
+ label: dataset.name,
160
+ description: dataset.num_rows ? `${dataset.num_rows.toLocaleString()} rows` : undefined
161
+ }))}
162
+ value={data.dataset_id ? parseInt(data.dataset_id) : null}
163
+ onChange={(value) => setData('dataset_id', value ? value.toString() : '')}
164
+ placeholder="Select a dataset"
165
+ />
166
+ {errors.dataset_id && (
167
+ <p className="mt-1 text-sm text-red-600">{errors.dataset_id}</p>
168
+ )}
169
+ </div>
170
+ )}
171
+
172
+ {(selectedOption || dataset_id) && (
173
+ <div className="mt-4">
174
+ <div
175
+ {...getRootProps()}
176
+ className={`w-full px-4 py-3 rounded-lg text-left transition-all duration-200 border-2 border-dashed cursor-pointer
177
+ ${data.config || isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-blue-500'}`}
178
+ >
179
+ <input {...getInputProps()} />
180
+ <div className="flex items-center justify-center gap-2 text-sm">
181
+ <Upload className={`w-4 h-4 ${data.config || isDragActive ? 'text-blue-600' : 'text-gray-400'}`} />
182
+ <span className={data.config || isDragActive ? 'text-blue-600' : 'text-gray-500'}>
183
+ {isDragActive ? 'Drop the file here' : data.config ? data.config.name : 'Click or drag to select configuration file'}
184
+ </span>
185
+ </div>
186
+ </div>
187
+ {errors.config && (
188
+ <p className="mt-1 text-sm text-red-600">{errors.config}</p>
189
+ )}
190
+ </div>
191
+ )}
192
+
193
+ <div className="mt-6 flex justify-end gap-3">
194
+ <button
195
+ onClick={onClose}
196
+ className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900"
197
+ >
198
+ Cancel
199
+ </button>
200
+ <button
201
+ onClick={handleUpload}
202
+ disabled={!canUpload || processing}
203
+ className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed inline-flex items-center gap-2"
204
+ >
205
+ {processing ? 'Uploading...' : 'Upload'}
206
+ <ArrowRight className="w-4 h-4" />
207
+ </button>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ );
212
+ }
@@ -0,0 +1,2 @@
1
+ export { DownloadModelModal } from './DownloadModelModal';
2
+ export { UploadModelModal } from './UploadModelModal';
@@ -1,36 +1,20 @@
1
1
  import React, { useState, useMemo, useEffect } from 'react';
2
2
  import { Link, usePage, router } from '@inertiajs/react';
3
- import { Database, Plus, Trash2, ExternalLink, Loader2, AlertCircle, ChevronDown, ChevronUp } from 'lucide-react';
3
+ import { Database, Plus, Upload } from 'lucide-react';
4
4
  import { EmptyState } from '../components/EmptyState';
5
5
  import { SearchInput } from '../components/SearchInput';
6
6
  import { Pagination } from '../components/Pagination';
7
- import { Dataset, DatasetWorkflowStatus, Column } from "@types/dataset";
7
+ import { DatasetCard } from '../components/DatasetCard';
8
+ import { UploadDatasetButton } from '../components/datasets/UploadDatasetButton';
9
+ import { Dataset } from "@types/dataset";
10
+
8
11
  interface Props {
9
12
  datasets: Dataset[];
10
13
  }
11
14
 
12
15
  const ITEMS_PER_PAGE = 6;
13
16
 
14
- const STATUS_STYLES: Record<DatasetWorkflowStatus, { bg: string; text: string; icon: React.ReactNode }> = {
15
- analyzing: {
16
- bg: 'bg-blue-100',
17
- text: 'text-blue-800',
18
- icon: <Loader2 className="w-4 h-4 animate-spin" />
19
- },
20
- ready: {
21
- bg: 'bg-green-100',
22
- text: 'text-green-800',
23
- icon: null
24
- },
25
- failed: {
26
- bg: 'bg-red-100',
27
- text: 'text-red-800',
28
- icon: <AlertCircle className="w-4 h-4" />
29
- },
30
- };
31
-
32
- export default function DatasetsPage({ datasets, constants }: Props) {
33
- console.log(datasets)
17
+ export default function DatasetsPage({ datasets }: Props) {
34
18
  const { rootPath } = usePage().props;
35
19
  const [searchQuery, setSearchQuery] = useState('');
36
20
  const [currentPage, setCurrentPage] = useState(1);
@@ -55,6 +39,17 @@ export default function DatasetsPage({ datasets, constants }: Props) {
55
39
  }
56
40
  };
57
41
 
42
+ const handleRefresh = (datasetId: number) => {
43
+ router.post(`${rootPath}/datasets/${datasetId}/refresh`);
44
+ };
45
+
46
+ const handleAbort = (datasetId: number) => {
47
+ router.post(`${rootPath}/datasets/${datasetId}/abort`, {}, {
48
+ preserveScroll: true,
49
+ preserveState: true
50
+ });
51
+ };
52
+
58
53
  useEffect(() => {
59
54
  let pollInterval: number | undefined;
60
55
 
@@ -111,13 +106,16 @@ export default function DatasetsPage({ datasets, constants }: Props) {
111
106
  placeholder="Search datasets..."
112
107
  />
113
108
  </div>
114
- <Link
115
- href={`${rootPath}/datasets/new`}
116
- 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"
117
- >
118
- <Plus className="w-4 h-4" />
119
- New Dataset
120
- </Link>
109
+ <div className="flex gap-3">
110
+ <UploadDatasetButton />
111
+ <Link
112
+ href={`${rootPath}/datasets/new`}
113
+ 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"
114
+ >
115
+ <Plus className="w-4 h-4" />
116
+ New Dataset
117
+ </Link>
118
+ </div>
121
119
  </div>
122
120
 
123
121
  {paginatedDatasets.length === 0 ? (
@@ -141,108 +139,16 @@ export default function DatasetsPage({ datasets, constants }: Props) {
141
139
  <>
142
140
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
143
141
  {paginatedDatasets.map((dataset) => (
144
- <div
142
+ <DatasetCard
145
143
  key={dataset.id}
146
- className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow"
147
- >
148
- <div className="flex justify-between items-start mb-4">
149
- <div className="flex items-start gap-3">
150
- <Database className="w-5 h-5 text-blue-600 mt-1" />
151
- <div>
152
- <div className="flex items-center gap-2">
153
- <h3 className="text-lg font-semibold text-gray-900">
154
- {dataset.name}
155
- </h3>
156
- <div className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${STATUS_STYLES[dataset.workflow_status].bg} ${STATUS_STYLES[dataset.workflow_status].text}`}>
157
- {STATUS_STYLES[dataset.workflow_status].icon}
158
- <span>{dataset.workflow_status.charAt(0).toUpperCase() + dataset.workflow_status.slice(1)}</span>
159
- </div>
160
- </div>
161
- <p className="text-sm text-gray-500 mt-1">
162
- {dataset.description}
163
- </p>
164
- </div>
165
- </div>
166
- <div className="flex gap-2">
167
- <Link
168
- href={`${rootPath}/datasets/${dataset.id}`}
169
- className={`transition-colors ${
170
- dataset.workflow_status === 'analyzing'
171
- ? 'text-gray-300 cursor-not-allowed pointer-events-none'
172
- : 'text-gray-400 hover:text-blue-600'
173
- }`}
174
- title={dataset.workflow_status === 'analyzing' ? 'Dataset is being analyzed' : 'View details'}
175
- >
176
- <ExternalLink className="w-5 h-5" />
177
- </Link>
178
- <button
179
- className="text-gray-400 hover:text-red-600 transition-colors"
180
- title="Delete dataset"
181
- onClick={() => handleDelete(dataset.id)}
182
- >
183
- <Trash2 className="w-5 h-5" />
184
- </button>
185
- </div>
186
- </div>
187
-
188
- <div className="grid grid-cols-2 gap-4 mt-4">
189
- <div>
190
- <span className="text-sm text-gray-500">Columns</span>
191
- <p className="text-sm font-medium text-gray-900">
192
- {dataset.columns.length} columns
193
- </p>
194
- </div>
195
- <div>
196
- <span className="text-sm text-gray-500">Rows</span>
197
- <p className="text-sm font-medium text-gray-900">
198
- {dataset.num_rows.toLocaleString()}
199
- </p>
200
- </div>
201
- </div>
202
-
203
- <div className="mt-4 pt-4 border-t border-gray-100">
204
- <div className="flex flex-wrap gap-2">
205
- {dataset.columns.slice(0, 3).map((column: Column) => (
206
- <span
207
- key={column.name}
208
- className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
209
- >
210
- {column.name}
211
- </span>
212
- ))}
213
- {dataset.columns.length > 3 && (
214
- <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
215
- +{dataset.columns.length - 3} more
216
- </span>
217
- )}
218
- </div>
219
- </div>
220
-
221
- {dataset.workflow_status === 'failed' && dataset.stacktrace && (
222
- <div className="mt-4 pt-4 border-t border-gray-100">
223
- <button
224
- onClick={() => toggleError(dataset.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(dataset.id) ? (
230
- <ChevronUp className="w-4 h-4" />
231
- ) : (
232
- <ChevronDown className="w-4 h-4" />
233
- )}
234
- </button>
235
- {expandedErrors.includes(dataset.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
- {dataset.stacktrace}
239
- </pre>
240
- </div>
241
- )}
242
- </div>
243
- )}
244
-
245
- </div>
144
+ dataset={dataset}
145
+ rootPath={rootPath}
146
+ onDelete={handleDelete}
147
+ onRefresh={handleRefresh}
148
+ onAbort={handleAbort}
149
+ isErrorExpanded={expandedErrors.includes(dataset.id)}
150
+ onToggleError={toggleError}
151
+ />
246
152
  ))}
247
153
  </div>
248
154
 
@@ -1,6 +1,6 @@
1
1
  import React, { useState, useMemo, useEffect } from 'react';
2
2
  import { Link, usePage, router } from '@inertiajs/react';
3
- import { HardDrive, Plus, Trash2, Settings, RefreshCw, ChevronDown, ChevronUp, AlertCircle } from 'lucide-react';
3
+ import { HardDrive, Plus, Trash2, Settings, RefreshCw, ChevronDown, ChevronUp, AlertCircle, XCircle } from 'lucide-react';
4
4
  import { EmptyState } from '../components/EmptyState';
5
5
  import { SearchInput } from '../components/SearchInput';
6
6
  import { Pagination } from '../components/Pagination';
@@ -62,6 +62,17 @@ export default function DatasourcesPage({ datasources }: { datasources: Datasour
62
62
  }
63
63
  };
64
64
 
65
+ const handleAbort = async (id: number) => {
66
+ try {
67
+ await router.post(`${rootPath}/datasources/${id}/abort`, {}, {
68
+ preserveScroll: true,
69
+ preserveState: true
70
+ });
71
+ } catch (error) {
72
+ console.error('Failed to abort datasource sync:', error);
73
+ }
74
+ };
75
+
65
76
  const formatLastSyncedAt = (lastSyncedAt: string) => {
66
77
  if (lastSyncedAt === 'Not Synced') return lastSyncedAt;
67
78
 
@@ -178,7 +189,7 @@ export default function DatasourcesPage({ datasources }: { datasources: Datasour
178
189
  </div>
179
190
  </div>
180
191
  <div className="flex gap-2">
181
- <button
192
+ <button
182
193
  onClick={() => handleSync(datasource.id)}
183
194
  disabled={datasource.is_syncing}
184
195
  className={`text-gray-400 hover:text-blue-600 transition-colors ${
@@ -188,6 +199,15 @@ export default function DatasourcesPage({ datasources }: { datasources: Datasour
188
199
  >
189
200
  <RefreshCw className="w-5 h-5" />
190
201
  </button>
202
+ {datasource.is_syncing && (
203
+ <button
204
+ onClick={() => handleAbort(datasource.id)}
205
+ className="text-gray-400 hover:text-red-600 transition-colors"
206
+ title="Abort sync"
207
+ >
208
+ <XCircle className="w-5 h-5" />
209
+ </button>
210
+ )}
191
211
  <Link
192
212
  href={`${rootPath}/datasources/${datasource.id}/edit`}
193
213
  className="text-gray-400 hover:text-blue-600 transition-colors"
@@ -1,17 +1,26 @@
1
1
  import React, { useState, useMemo, useEffect } from 'react';
2
- import { Brain, Plus, Trash2 } from 'lucide-react';
2
+ import { Brain, Plus, Upload } from 'lucide-react';
3
3
  import { ModelCard } from '../components/ModelCard';
4
4
  import { EmptyState } from '../components/EmptyState';
5
5
  import { SearchInput } from '../components/SearchInput';
6
6
  import { Pagination } from '../components/Pagination';
7
7
  import { router } from '@inertiajs/react';
8
+ import type { Model, Dataset } from '../types';
9
+ import { UploadModelModal } from '../components/models';
8
10
 
9
11
  const ITEMS_PER_PAGE = 6;
10
12
 
11
- export default function ModelsPage({ rootPath, models }) {
13
+ interface ModelsPageProps {
14
+ rootPath: string;
15
+ models: Array<Model>;
16
+ datasets: Array<Dataset>;
17
+ }
18
+
19
+ export default function ModelsPage({ rootPath, models, datasets }: ModelsPageProps) {
12
20
  const [selectedModelId, setSelectedModelId] = useState<number | null>(null);
13
21
  const [searchQuery, setSearchQuery] = useState('');
14
22
  const [currentPage, setCurrentPage] = useState(1);
23
+ const [showUploadModal, setShowUploadModal] = useState(false);
15
24
 
16
25
  const filteredModels = useMemo(() => {
17
26
  return models.filter(model =>
@@ -60,13 +69,23 @@ export default function ModelsPage({ rootPath, models }) {
60
69
  placeholder="Search models..."
61
70
  />
62
71
  </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>
72
+ <div className="flex gap-3">
73
+ <button
74
+ onClick={() => setShowUploadModal(true)}
75
+ className="inline-flex items-center gap-2 px-4 py-2 bg-white border border-gray-300 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-50"
76
+ title="Import model"
77
+ >
78
+ <Upload className="w-4 h-4" />
79
+ Import
80
+ </button>
81
+ <button
82
+ onClick={() => router.visit(`${rootPath}/models/new`)}
83
+ 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"
84
+ >
85
+ <Plus className="w-4 h-4" />
86
+ New Model
87
+ </button>
88
+ </div>
70
89
  </div>
71
90
 
72
91
  {paginatedModels.length === 0 ? (
@@ -91,11 +110,12 @@ export default function ModelsPage({ rootPath, models }) {
91
110
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
92
111
  {paginatedModels.map((model) => (
93
112
  <ModelCard
94
- rootPath={rootPath}
95
113
  key={model.id}
96
114
  initialModel={model}
97
- onViewDetails={setSelectedModelId}
115
+ onViewDetails={() => setSelectedModelId(model.id)}
98
116
  handleDelete={handleDelete}
117
+ rootPath={rootPath}
118
+ datasets={datasets}
99
119
  />
100
120
  ))}
101
121
  </div>
@@ -110,6 +130,12 @@ export default function ModelsPage({ rootPath, models }) {
110
130
  </>
111
131
  )}
112
132
  </div>
133
+
134
+ <UploadModelModal
135
+ isOpen={showUploadModal}
136
+ onClose={() => setShowUploadModal(false)}
137
+ datasets={datasets}
138
+ />
113
139
  </div>
114
140
  );
115
141
  }
@@ -1,5 +1,4 @@
1
- import type { Datasource } from "./datasource";
2
- import type { SplitterConfig } from '../components/dataset/splitters/types';
1
+ export type { Datasource } from "./datasource";
3
2
 
4
3
  export type DatasetWorkflowStatus = "analyzing" | "ready" | "failed" | "locked";
5
4
  export type DatasetStatus = "training" | "inference" | "retired";
@@ -1,4 +1,4 @@
1
- import { Dataset } from './dataset';
1
+ export type { Dataset } from './types/dataset';
2
2
 
3
3
  export type ModelStatus = 'success' | 'failed';
4
4
  export type DeploymentStatus = 'training' | 'inference' | 'retired';
@@ -10,13 +10,13 @@ module EasyML
10
10
 
11
11
  @last_activity = Time.current
12
12
  setup_signal_traps
13
- # @monitor_thread = start_monitor_thread
13
+ @monitor_thread = start_monitor_thread
14
14
 
15
15
  @model.actually_train do |iteration_info|
16
16
  @last_activity = Time.current
17
17
  end
18
18
  ensure
19
- # @monitor_thread&.exit
19
+ @monitor_thread&.exit
20
20
  @model.unlock!
21
21
  end
22
22
 
@@ -27,6 +27,10 @@ module EasyML
27
27
  @preprocessing_step = preprocessing_step.with_indifferent_access
28
28
  end
29
29
 
30
+ def expr
31
+ Polars.col(column.name)
32
+ end
33
+
30
34
  def applies?
31
35
  method_applies? || param_applies?
32
36
  end
@@ -10,10 +10,12 @@ module EasyML
10
10
  "Clip"
11
11
  end
12
12
 
13
+ def expr
14
+ Polars.col(column.name).clip(min, max).alias(column.name)
15
+ end
16
+
13
17
  def transform(df)
14
- df = df.with_column(
15
- Polars.col(column.name).clip(min, max).alias(column.name)
16
- )
18
+ df = df.with_column(expr)
17
19
  df
18
20
  end
19
21
 
@@ -2,18 +2,23 @@ module EasyML
2
2
  class Column
3
3
  class Imputers
4
4
  class Imputer
5
- attr_accessor :dataset, :column, :preprocessing_step
5
+ attr_accessor :dataset, :column, :preprocessing_step, :allowed_adapters
6
6
 
7
- def initialize(column, preprocessing_step)
7
+ def initialize(column, preprocessing_step, allowed_adapters = [])
8
8
  @column = column
9
9
  @dataset = column.dataset
10
10
  @preprocessing_step = preprocessing_step.with_indifferent_access
11
+ @allowed_adapters = allowed_adapters.map(&:to_sym)
11
12
  end
12
13
 
13
14
  def inspect
14
15
  "#<#{self.class.name} adapters=#{adapters.map(&:inspect).join(", ")}>"
15
16
  end
16
17
 
18
+ def exprs
19
+ adapters.map(&:expr)
20
+ end
21
+
17
22
  def ordered_adapters
18
23
  [
19
24
  Clip,
@@ -29,19 +34,12 @@ module EasyML
29
34
  ]
30
35
  end
31
36
 
32
- def adapters
33
- @adapters ||= ordered_adapters.map { |klass| klass.new(column, preprocessing_step) }.select(&:applies?)
37
+ def allowed?(adapter)
38
+ allowed_adapters.empty? || allowed_adapters.include?(adapter.class.name.split("::").last.underscore.to_sym)
34
39
  end
35
40
 
36
- def imputers
37
- return nil if column.preprocessing_steps.blank?
38
-
39
- @imputers ||= column.preprocessing_steps.keys.reduce({}) do |hash, key|
40
- hash[key.to_sym] = Imputer.new(
41
- column: column,
42
- preprocessing_step: column.preprocessing_steps[key],
43
- )
44
- end
41
+ def adapters
42
+ @adapters ||= ordered_adapters.map { |klass| klass.new(column, preprocessing_step) }.select { |adapter| allowed?(adapter) && adapter.applies? }
45
43
  end
46
44
 
47
45
  def description