easy_ml 0.2.0.pre.rc71 → 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 (117) 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 +46 -0
  5. data/app/frontend/components/DatasetCard.tsx +212 -0
  6. data/app/frontend/components/ModelCard.tsx +114 -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/reaper.rb +55 -0
  19. data/app/jobs/easy_ml/training_job.rb +1 -1
  20. data/app/models/easy_ml/column/imputers/base.rb +4 -0
  21. data/app/models/easy_ml/column/imputers/clip.rb +5 -3
  22. data/app/models/easy_ml/column/imputers/imputer.rb +11 -13
  23. data/app/models/easy_ml/column/imputers/mean.rb +7 -3
  24. data/app/models/easy_ml/column/imputers/null_imputer.rb +3 -0
  25. data/app/models/easy_ml/column/imputers/ordinal_encoder.rb +5 -1
  26. data/app/models/easy_ml/column/imputers.rb +3 -1
  27. data/app/models/easy_ml/column/lineage/base.rb +5 -1
  28. data/app/models/easy_ml/column/lineage/computed_by_feature.rb +1 -1
  29. data/app/models/easy_ml/column/lineage/preprocessed.rb +1 -1
  30. data/app/models/easy_ml/column/lineage/raw_dataset.rb +1 -1
  31. data/app/models/easy_ml/column/selector.rb +4 -0
  32. data/app/models/easy_ml/column.rb +79 -63
  33. data/app/models/easy_ml/column_history.rb +28 -28
  34. data/app/models/easy_ml/column_list/imputer.rb +23 -0
  35. data/app/models/easy_ml/column_list.rb +39 -26
  36. data/app/models/easy_ml/dataset/learner/base.rb +34 -0
  37. data/app/models/easy_ml/dataset/learner/eager/boolean.rb +10 -0
  38. data/app/models/easy_ml/dataset/learner/eager/categorical.rb +51 -0
  39. data/app/models/easy_ml/dataset/learner/eager/query.rb +37 -0
  40. data/app/models/easy_ml/dataset/learner/eager.rb +43 -0
  41. data/app/models/easy_ml/dataset/learner/lazy/boolean.rb +13 -0
  42. data/app/models/easy_ml/dataset/learner/lazy/categorical.rb +10 -0
  43. data/app/models/easy_ml/dataset/learner/lazy/datetime.rb +19 -0
  44. data/app/models/easy_ml/dataset/learner/lazy/null.rb +17 -0
  45. data/app/models/easy_ml/dataset/learner/lazy/numeric.rb +19 -0
  46. data/app/models/easy_ml/dataset/learner/lazy/query.rb +69 -0
  47. data/app/models/easy_ml/dataset/learner/lazy/string.rb +19 -0
  48. data/app/models/easy_ml/dataset/learner/lazy.rb +51 -0
  49. data/app/models/easy_ml/dataset/learner/query.rb +25 -0
  50. data/app/models/easy_ml/dataset/learner.rb +100 -0
  51. data/app/models/easy_ml/dataset.rb +150 -36
  52. data/app/models/easy_ml/dataset_history.rb +1 -0
  53. data/app/models/easy_ml/datasource.rb +9 -0
  54. data/app/models/easy_ml/event.rb +5 -7
  55. data/app/models/easy_ml/export/column.rb +27 -0
  56. data/app/models/easy_ml/export/dataset.rb +37 -0
  57. data/app/models/easy_ml/export/datasource.rb +12 -0
  58. data/app/models/easy_ml/export/feature.rb +24 -0
  59. data/app/models/easy_ml/export/model.rb +40 -0
  60. data/app/models/easy_ml/export/retraining_job.rb +20 -0
  61. data/app/models/easy_ml/export/splitter.rb +14 -0
  62. data/app/models/easy_ml/feature.rb +21 -0
  63. data/app/models/easy_ml/import/column.rb +35 -0
  64. data/app/models/easy_ml/import/dataset.rb +148 -0
  65. data/app/models/easy_ml/import/feature.rb +36 -0
  66. data/app/models/easy_ml/import/model.rb +136 -0
  67. data/app/models/easy_ml/import/retraining_job.rb +29 -0
  68. data/app/models/easy_ml/import/splitter.rb +34 -0
  69. data/app/models/easy_ml/lineage.rb +44 -0
  70. data/app/models/easy_ml/model.rb +101 -37
  71. data/app/models/easy_ml/model_file.rb +6 -0
  72. data/app/models/easy_ml/models/xgboost/evals_callback.rb +7 -7
  73. data/app/models/easy_ml/models/xgboost.rb +33 -9
  74. data/app/models/easy_ml/retraining_job.rb +8 -1
  75. data/app/models/easy_ml/retraining_run.rb +7 -5
  76. data/app/models/easy_ml/splitter.rb +8 -0
  77. data/app/models/lineage_history.rb +6 -0
  78. data/app/serializers/easy_ml/column_serializer.rb +7 -1
  79. data/app/serializers/easy_ml/dataset_serializer.rb +2 -1
  80. data/app/serializers/easy_ml/lineage_serializer.rb +9 -0
  81. data/config/routes.rb +14 -1
  82. data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +3 -3
  83. data/lib/easy_ml/core/tuner.rb +13 -12
  84. data/lib/easy_ml/data/polars_column.rb +149 -100
  85. data/lib/easy_ml/data/polars_reader.rb +8 -5
  86. data/lib/easy_ml/data/polars_schema.rb +56 -0
  87. data/lib/easy_ml/data/splits/file_split.rb +20 -2
  88. data/lib/easy_ml/data/splits/split.rb +10 -1
  89. data/lib/easy_ml/data.rb +1 -0
  90. data/lib/easy_ml/deep_compact.rb +19 -0
  91. data/lib/easy_ml/engine.rb +1 -0
  92. data/lib/easy_ml/feature_store.rb +2 -6
  93. data/lib/easy_ml/railtie/generators/migration/migration_generator.rb +6 -0
  94. data/lib/easy_ml/railtie/templates/migration/add_extra_metadata_to_columns.rb.tt +9 -0
  95. data/lib/easy_ml/railtie/templates/migration/add_raw_schema_to_datasets.rb.tt +9 -0
  96. data/lib/easy_ml/railtie/templates/migration/add_unique_constraint_to_easy_ml_model_names.rb.tt +8 -0
  97. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_lineages.rb.tt +24 -0
  98. data/lib/easy_ml/railtie/templates/migration/remove_evaluator_from_retraining_jobs.rb.tt +7 -0
  99. data/lib/easy_ml/railtie/templates/migration/update_preprocessing_steps_to_jsonb.rb.tt +18 -0
  100. data/lib/easy_ml/timing.rb +34 -0
  101. data/lib/easy_ml/version.rb +1 -1
  102. data/lib/easy_ml.rb +2 -0
  103. data/public/easy_ml/assets/.vite/manifest.json +2 -2
  104. data/public/easy_ml/assets/assets/Application-Q7L6ioxr.css +1 -0
  105. data/public/easy_ml/assets/assets/entrypoints/Application.tsx-Rrzo4ecT.js +522 -0
  106. data/public/easy_ml/assets/assets/entrypoints/Application.tsx-Rrzo4ecT.js.map +1 -0
  107. metadata +53 -12
  108. data/app/models/easy_ml/column/learners/base.rb +0 -103
  109. data/app/models/easy_ml/column/learners/boolean.rb +0 -11
  110. data/app/models/easy_ml/column/learners/categorical.rb +0 -51
  111. data/app/models/easy_ml/column/learners/datetime.rb +0 -19
  112. data/app/models/easy_ml/column/learners/null.rb +0 -22
  113. data/app/models/easy_ml/column/learners/numeric.rb +0 -33
  114. data/app/models/easy_ml/column/learners/string.rb +0 -15
  115. data/public/easy_ml/assets/assets/Application-BbFobaXt.css +0 -1
  116. data/public/easy_ml/assets/assets/entrypoints/Application.tsx-CibZcrBc.js +0 -489
  117. data/public/easy_ml/assets/assets/entrypoints/Application.tsx-CibZcrBc.js.map +0 -1
@@ -0,0 +1,90 @@
1
+ import React, { useState } from 'react';
2
+ import { FileDown, FileJson, Database, ArrowRight } from 'lucide-react';
3
+ import { router, usePage } from '@inertiajs/react';
4
+
5
+ interface DownloadModelModalProps {
6
+ isOpen: boolean;
7
+ onClose: () => void;
8
+ modelId: number;
9
+ }
10
+
11
+ export function DownloadModelModal({ isOpen, onClose, modelId }: DownloadModelModalProps) {
12
+ const { rootPath } = usePage().props;
13
+ const [selectedOption, setSelectedOption] = useState<'model' | 'both' | null>(null);
14
+
15
+ if (!isOpen) return null;
16
+
17
+ const handleDownload = async () => {
18
+ if (!selectedOption) return;
19
+
20
+ const includeDataset = selectedOption === 'both';
21
+ window.location.href = `${rootPath}/models/${modelId}/download?include_dataset=${includeDataset}`;
22
+ onClose();
23
+ };
24
+
25
+ return (
26
+ <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50">
27
+ <div className="bg-white rounded-xl p-6 w-full max-w-md shadow-2xl">
28
+ <div className="flex items-center justify-between mb-6">
29
+ <h3 className="text-lg font-semibold text-gray-900">Download Configuration</h3>
30
+ <div className="p-2 bg-blue-50 rounded-lg">
31
+ <FileDown className="w-5 h-5 text-blue-600" />
32
+ </div>
33
+ </div>
34
+
35
+ <div className="space-y-3">
36
+ <button
37
+ onClick={() => setSelectedOption('model')}
38
+ className={`w-full px-4 py-3 rounded-lg text-left transition-all duration-200 ${
39
+ selectedOption === 'model'
40
+ ? 'bg-blue-50 border-2 border-blue-500 ring-2 ring-blue-200'
41
+ : 'bg-white border-2 border-gray-200 hover:border-blue-200'
42
+ }`}
43
+ >
44
+ <div className="flex items-center justify-between">
45
+ <div>
46
+ <div className="font-medium text-gray-900">Model Only</div>
47
+ <div className="text-sm text-gray-500">Download model configuration without dataset details</div>
48
+ </div>
49
+ <FileJson className={`w-5 h-5 ${selectedOption === 'model' ? 'text-blue-600' : 'text-gray-400'}`} />
50
+ </div>
51
+ </button>
52
+
53
+ <button
54
+ onClick={() => setSelectedOption('both')}
55
+ className={`w-full px-4 py-3 rounded-lg text-left transition-all duration-200 ${
56
+ selectedOption === 'both'
57
+ ? 'bg-blue-50 border-2 border-blue-500 ring-2 ring-blue-200'
58
+ : 'bg-white border-2 border-gray-200 hover:border-blue-200'
59
+ }`}
60
+ >
61
+ <div className="flex items-center justify-between">
62
+ <div>
63
+ <div className="font-medium text-gray-900">Model + Dataset</div>
64
+ <div className="text-sm text-gray-500">Download complete configuration including dataset details</div>
65
+ </div>
66
+ <Database className={`w-5 h-5 ${selectedOption === 'both' ? 'text-blue-600' : 'text-gray-400'}`} />
67
+ </div>
68
+ </button>
69
+ </div>
70
+
71
+ <div className="mt-6 flex justify-end gap-3">
72
+ <button
73
+ onClick={onClose}
74
+ className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900"
75
+ >
76
+ Cancel
77
+ </button>
78
+ <button
79
+ onClick={handleDownload}
80
+ disabled={!selectedOption}
81
+ 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"
82
+ >
83
+ Download
84
+ <ArrowRight className="w-4 h-4" />
85
+ </button>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ );
90
+ }
@@ -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';