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.
- checksums.yaml +4 -4
- data/app/controllers/easy_ml/datasets_controller.rb +33 -0
- data/app/controllers/easy_ml/datasources_controller.rb +7 -0
- data/app/controllers/easy_ml/models_controller.rb +38 -0
- data/app/frontend/components/DatasetCard.tsx +212 -0
- data/app/frontend/components/ModelCard.tsx +69 -29
- data/app/frontend/components/StackTrace.tsx +13 -0
- data/app/frontend/components/dataset/FeatureConfigPopover.tsx +10 -7
- data/app/frontend/components/datasets/UploadDatasetButton.tsx +51 -0
- data/app/frontend/components/models/DownloadModelModal.tsx +90 -0
- data/app/frontend/components/models/UploadModelModal.tsx +212 -0
- data/app/frontend/components/models/index.ts +2 -0
- data/app/frontend/pages/DatasetsPage.tsx +36 -130
- data/app/frontend/pages/DatasourcesPage.tsx +22 -2
- data/app/frontend/pages/ModelsPage.tsx +37 -11
- data/app/frontend/types/dataset.ts +1 -2
- data/app/frontend/types.ts +1 -1
- data/app/jobs/easy_ml/training_job.rb +2 -2
- data/app/models/easy_ml/column/imputers/base.rb +4 -0
- data/app/models/easy_ml/column/imputers/clip.rb +5 -3
- data/app/models/easy_ml/column/imputers/imputer.rb +11 -13
- data/app/models/easy_ml/column/imputers/mean.rb +7 -3
- data/app/models/easy_ml/column/imputers/null_imputer.rb +3 -0
- data/app/models/easy_ml/column/imputers/ordinal_encoder.rb +5 -1
- data/app/models/easy_ml/column/imputers.rb +3 -1
- data/app/models/easy_ml/column/lineage/base.rb +5 -1
- data/app/models/easy_ml/column/lineage/computed_by_feature.rb +1 -1
- data/app/models/easy_ml/column/lineage/preprocessed.rb +1 -1
- data/app/models/easy_ml/column/lineage/raw_dataset.rb +1 -1
- data/app/models/easy_ml/column/selector.rb +4 -0
- data/app/models/easy_ml/column.rb +79 -63
- data/app/models/easy_ml/column_history.rb +28 -28
- data/app/models/easy_ml/column_list/imputer.rb +23 -0
- data/app/models/easy_ml/column_list.rb +39 -26
- data/app/models/easy_ml/dataset/learner/base.rb +34 -0
- data/app/models/easy_ml/dataset/learner/eager/boolean.rb +10 -0
- data/app/models/easy_ml/dataset/learner/eager/categorical.rb +51 -0
- data/app/models/easy_ml/dataset/learner/eager/query.rb +37 -0
- data/app/models/easy_ml/dataset/learner/eager.rb +43 -0
- data/app/models/easy_ml/dataset/learner/lazy/boolean.rb +13 -0
- data/app/models/easy_ml/dataset/learner/lazy/categorical.rb +10 -0
- data/app/models/easy_ml/dataset/learner/lazy/datetime.rb +19 -0
- data/app/models/easy_ml/dataset/learner/lazy/null.rb +17 -0
- data/app/models/easy_ml/dataset/learner/lazy/numeric.rb +19 -0
- data/app/models/easy_ml/dataset/learner/lazy/query.rb +69 -0
- data/app/models/easy_ml/dataset/learner/lazy/string.rb +19 -0
- data/app/models/easy_ml/dataset/learner/lazy.rb +51 -0
- data/app/models/easy_ml/dataset/learner/query.rb +25 -0
- data/app/models/easy_ml/dataset/learner.rb +100 -0
- data/app/models/easy_ml/dataset.rb +150 -36
- data/app/models/easy_ml/dataset_history.rb +1 -0
- data/app/models/easy_ml/datasource.rb +9 -0
- data/app/models/easy_ml/event.rb +4 -0
- data/app/models/easy_ml/export/column.rb +27 -0
- data/app/models/easy_ml/export/dataset.rb +37 -0
- data/app/models/easy_ml/export/datasource.rb +12 -0
- data/app/models/easy_ml/export/feature.rb +24 -0
- data/app/models/easy_ml/export/model.rb +40 -0
- data/app/models/easy_ml/export/retraining_job.rb +20 -0
- data/app/models/easy_ml/export/splitter.rb +14 -0
- data/app/models/easy_ml/feature.rb +21 -0
- data/app/models/easy_ml/import/column.rb +35 -0
- data/app/models/easy_ml/import/dataset.rb +148 -0
- data/app/models/easy_ml/import/feature.rb +36 -0
- data/app/models/easy_ml/import/model.rb +136 -0
- data/app/models/easy_ml/import/retraining_job.rb +29 -0
- data/app/models/easy_ml/import/splitter.rb +34 -0
- data/app/models/easy_ml/lineage.rb +44 -0
- data/app/models/easy_ml/model.rb +93 -36
- data/app/models/easy_ml/model_file.rb +6 -0
- data/app/models/easy_ml/models/xgboost/evals_callback.rb +7 -7
- data/app/models/easy_ml/models/xgboost.rb +33 -9
- data/app/models/easy_ml/retraining_job.rb +8 -1
- data/app/models/easy_ml/retraining_run.rb +6 -4
- data/app/models/easy_ml/splitter.rb +8 -0
- data/app/models/lineage_history.rb +6 -0
- data/app/serializers/easy_ml/column_serializer.rb +7 -1
- data/app/serializers/easy_ml/dataset_serializer.rb +2 -1
- data/app/serializers/easy_ml/lineage_serializer.rb +9 -0
- data/config/routes.rb +13 -1
- data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +3 -3
- data/lib/easy_ml/core/tuner.rb +12 -11
- data/lib/easy_ml/data/polars_column.rb +149 -100
- data/lib/easy_ml/data/polars_reader.rb +8 -5
- data/lib/easy_ml/data/polars_schema.rb +56 -0
- data/lib/easy_ml/data/splits/file_split.rb +20 -2
- data/lib/easy_ml/data/splits/split.rb +10 -1
- data/lib/easy_ml/data.rb +1 -0
- data/lib/easy_ml/deep_compact.rb +19 -0
- data/lib/easy_ml/feature_store.rb +2 -6
- data/lib/easy_ml/railtie/generators/migration/migration_generator.rb +6 -0
- data/lib/easy_ml/railtie/templates/migration/add_extra_metadata_to_columns.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/add_raw_schema_to_datasets.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/add_unique_constraint_to_easy_ml_model_names.rb.tt +8 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_lineages.rb.tt +24 -0
- data/lib/easy_ml/railtie/templates/migration/remove_evaluator_from_retraining_jobs.rb.tt +7 -0
- data/lib/easy_ml/railtie/templates/migration/update_preprocessing_steps_to_jsonb.rb.tt +18 -0
- data/lib/easy_ml/timing.rb +34 -0
- data/lib/easy_ml/version.rb +1 -1
- data/lib/easy_ml.rb +2 -0
- data/public/easy_ml/assets/.vite/manifest.json +2 -2
- data/public/easy_ml/assets/assets/Application-Q7L6ioxr.css +1 -0
- data/public/easy_ml/assets/assets/entrypoints/Application.tsx-Rrzo4ecT.js +522 -0
- data/public/easy_ml/assets/assets/entrypoints/Application.tsx-Rrzo4ecT.js.map +1 -0
- metadata +52 -12
- data/app/models/easy_ml/column/learners/base.rb +0 -103
- data/app/models/easy_ml/column/learners/boolean.rb +0 -11
- data/app/models/easy_ml/column/learners/categorical.rb +0 -51
- data/app/models/easy_ml/column/learners/datetime.rb +0 -19
- data/app/models/easy_ml/column/learners/null.rb +0 -22
- data/app/models/easy_ml/column/learners/numeric.rb +0 -33
- data/app/models/easy_ml/column/learners/string.rb +0 -15
- data/public/easy_ml/assets/assets/Application-B3sRjyMT.css +0 -1
- data/public/easy_ml/assets/assets/entrypoints/Application.tsx-Dfg-nTrB.js +0 -489
- 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
|
+
}
|
@@ -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,
|
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 {
|
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
|
-
|
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
|
-
<
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
<
|
142
|
+
<DatasetCard
|
145
143
|
key={dataset.id}
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
<
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
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";
|
data/app/frontend/types.ts
CHANGED
@@ -10,13 +10,13 @@ module EasyML
|
|
10
10
|
|
11
11
|
@last_activity = Time.current
|
12
12
|
setup_signal_traps
|
13
|
-
|
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
|
-
|
19
|
+
@monitor_thread&.exit
|
20
20
|
@model.unlock!
|
21
21
|
end
|
22
22
|
|
@@ -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
|
33
|
-
|
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
|
37
|
-
|
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
|