easy_ml 0.1.4 → 0.2.0.pre.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +234 -26
- data/Rakefile +45 -0
- data/app/controllers/easy_ml/application_controller.rb +67 -0
- data/app/controllers/easy_ml/columns_controller.rb +38 -0
- data/app/controllers/easy_ml/datasets_controller.rb +156 -0
- data/app/controllers/easy_ml/datasources_controller.rb +88 -0
- data/app/controllers/easy_ml/deploys_controller.rb +20 -0
- data/app/controllers/easy_ml/models_controller.rb +151 -0
- data/app/controllers/easy_ml/retraining_runs_controller.rb +19 -0
- data/app/controllers/easy_ml/settings_controller.rb +59 -0
- data/app/frontend/components/AlertProvider.tsx +108 -0
- data/app/frontend/components/DatasetPreview.tsx +161 -0
- data/app/frontend/components/EmptyState.tsx +28 -0
- data/app/frontend/components/ModelCard.tsx +255 -0
- data/app/frontend/components/ModelDetails.tsx +334 -0
- data/app/frontend/components/ModelForm.tsx +384 -0
- data/app/frontend/components/Navigation.tsx +300 -0
- data/app/frontend/components/Pagination.tsx +72 -0
- data/app/frontend/components/Popover.tsx +55 -0
- data/app/frontend/components/PredictionStream.tsx +105 -0
- data/app/frontend/components/ScheduleModal.tsx +726 -0
- data/app/frontend/components/SearchInput.tsx +23 -0
- data/app/frontend/components/SearchableSelect.tsx +132 -0
- data/app/frontend/components/dataset/AutosaveIndicator.tsx +39 -0
- data/app/frontend/components/dataset/ColumnConfigModal.tsx +431 -0
- data/app/frontend/components/dataset/ColumnFilters.tsx +256 -0
- data/app/frontend/components/dataset/ColumnList.tsx +101 -0
- data/app/frontend/components/dataset/FeatureConfigPopover.tsx +57 -0
- data/app/frontend/components/dataset/FeaturePicker.tsx +205 -0
- data/app/frontend/components/dataset/PreprocessingConfig.tsx +704 -0
- data/app/frontend/components/dataset/SplitConfigurator.tsx +120 -0
- data/app/frontend/components/dataset/splitters/DateSplitter.tsx +58 -0
- data/app/frontend/components/dataset/splitters/KFoldSplitter.tsx +68 -0
- data/app/frontend/components/dataset/splitters/LeavePOutSplitter.tsx +29 -0
- data/app/frontend/components/dataset/splitters/PredefinedSplitter.tsx +146 -0
- data/app/frontend/components/dataset/splitters/RandomSplitter.tsx +85 -0
- data/app/frontend/components/dataset/splitters/StratifiedSplitter.tsx +79 -0
- data/app/frontend/components/dataset/splitters/constants.ts +77 -0
- data/app/frontend/components/dataset/splitters/types.ts +168 -0
- data/app/frontend/components/dataset/splitters/utils.ts +53 -0
- data/app/frontend/components/features/CodeEditor.tsx +46 -0
- data/app/frontend/components/features/DataPreview.tsx +150 -0
- data/app/frontend/components/features/FeatureCard.tsx +88 -0
- data/app/frontend/components/features/FeatureForm.tsx +235 -0
- data/app/frontend/components/features/FeatureGroupCard.tsx +54 -0
- data/app/frontend/components/settings/PluginSettings.tsx +81 -0
- data/app/frontend/components/ui/badge.tsx +44 -0
- data/app/frontend/components/ui/collapsible.tsx +9 -0
- data/app/frontend/components/ui/scroll-area.tsx +46 -0
- data/app/frontend/components/ui/separator.tsx +29 -0
- data/app/frontend/entrypoints/App.tsx +40 -0
- data/app/frontend/entrypoints/Application.tsx +24 -0
- data/app/frontend/hooks/useAutosave.ts +61 -0
- data/app/frontend/layouts/Layout.tsx +38 -0
- data/app/frontend/lib/utils.ts +6 -0
- data/app/frontend/mockData.ts +272 -0
- data/app/frontend/pages/DatasetDetailsPage.tsx +103 -0
- data/app/frontend/pages/DatasetsPage.tsx +261 -0
- data/app/frontend/pages/DatasourceFormPage.tsx +147 -0
- data/app/frontend/pages/DatasourcesPage.tsx +261 -0
- data/app/frontend/pages/EditModelPage.tsx +45 -0
- data/app/frontend/pages/EditTransformationPage.tsx +56 -0
- data/app/frontend/pages/ModelsPage.tsx +115 -0
- data/app/frontend/pages/NewDatasetPage.tsx +366 -0
- data/app/frontend/pages/NewModelPage.tsx +45 -0
- data/app/frontend/pages/NewTransformationPage.tsx +43 -0
- data/app/frontend/pages/SettingsPage.tsx +272 -0
- data/app/frontend/pages/ShowModelPage.tsx +30 -0
- data/app/frontend/pages/TransformationsPage.tsx +95 -0
- data/app/frontend/styles/application.css +100 -0
- data/app/frontend/types/dataset.ts +146 -0
- data/app/frontend/types/datasource.ts +33 -0
- data/app/frontend/types/preprocessing.ts +1 -0
- data/app/frontend/types.ts +113 -0
- data/app/helpers/easy_ml/application_helper.rb +10 -0
- data/app/jobs/easy_ml/application_job.rb +21 -0
- data/app/jobs/easy_ml/batch_job.rb +46 -0
- data/app/jobs/easy_ml/compute_feature_job.rb +19 -0
- data/app/jobs/easy_ml/deploy_job.rb +13 -0
- data/app/jobs/easy_ml/finalize_feature_job.rb +15 -0
- data/app/jobs/easy_ml/refresh_dataset_job.rb +32 -0
- data/app/jobs/easy_ml/schedule_retraining_job.rb +11 -0
- data/app/jobs/easy_ml/sync_datasource_job.rb +17 -0
- data/app/jobs/easy_ml/training_job.rb +62 -0
- data/app/models/easy_ml/adapters/base_adapter.rb +45 -0
- data/app/models/easy_ml/adapters/polars_adapter.rb +77 -0
- data/app/models/easy_ml/cleaner.rb +82 -0
- data/app/models/easy_ml/column.rb +124 -0
- data/app/models/easy_ml/column_history.rb +30 -0
- data/app/models/easy_ml/column_list.rb +122 -0
- data/app/models/easy_ml/concerns/configurable.rb +61 -0
- data/app/models/easy_ml/concerns/versionable.rb +19 -0
- data/app/models/easy_ml/dataset.rb +767 -0
- data/app/models/easy_ml/dataset_history.rb +56 -0
- data/app/models/easy_ml/datasource.rb +182 -0
- data/app/models/easy_ml/datasource_history.rb +24 -0
- data/app/models/easy_ml/datasources/base_datasource.rb +54 -0
- data/app/models/easy_ml/datasources/file_datasource.rb +58 -0
- data/app/models/easy_ml/datasources/polars_datasource.rb +89 -0
- data/app/models/easy_ml/datasources/s3_datasource.rb +97 -0
- data/app/models/easy_ml/deploy.rb +114 -0
- data/app/models/easy_ml/event.rb +79 -0
- data/app/models/easy_ml/feature.rb +437 -0
- data/app/models/easy_ml/feature_history.rb +38 -0
- data/app/models/easy_ml/model.rb +575 -41
- data/app/models/easy_ml/model_file.rb +133 -0
- data/app/models/easy_ml/model_file_history.rb +24 -0
- data/app/models/easy_ml/model_history.rb +51 -0
- data/app/models/easy_ml/models/base_model.rb +58 -0
- data/app/models/easy_ml/models/hyperparameters/base.rb +99 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost/dart.rb +82 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost/gblinear.rb +82 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost/gbtree.rb +97 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost.rb +71 -0
- data/app/models/easy_ml/models/xgboost/evals_callback.rb +138 -0
- data/app/models/easy_ml/models/xgboost/progress_callback.rb +39 -0
- data/app/models/easy_ml/models/xgboost.rb +544 -5
- data/app/models/easy_ml/prediction.rb +44 -0
- data/app/models/easy_ml/retraining_job.rb +278 -0
- data/app/models/easy_ml/retraining_run.rb +184 -0
- data/app/models/easy_ml/settings.rb +37 -0
- data/app/models/easy_ml/splitter.rb +90 -0
- data/app/models/easy_ml/splitters/base_splitter.rb +28 -0
- data/app/models/easy_ml/splitters/date_splitter.rb +91 -0
- data/app/models/easy_ml/splitters/predefined_splitter.rb +74 -0
- data/app/models/easy_ml/splitters/random_splitter.rb +82 -0
- data/app/models/easy_ml/tuner_job.rb +56 -0
- data/app/models/easy_ml/tuner_run.rb +31 -0
- data/app/models/splitter_history.rb +6 -0
- data/app/serializers/easy_ml/column_serializer.rb +27 -0
- data/app/serializers/easy_ml/dataset_serializer.rb +73 -0
- data/app/serializers/easy_ml/datasource_serializer.rb +64 -0
- data/app/serializers/easy_ml/feature_serializer.rb +27 -0
- data/app/serializers/easy_ml/model_serializer.rb +90 -0
- data/app/serializers/easy_ml/retraining_job_serializer.rb +22 -0
- data/app/serializers/easy_ml/retraining_run_serializer.rb +39 -0
- data/app/serializers/easy_ml/settings_serializer.rb +9 -0
- data/app/views/layouts/easy_ml/application.html.erb +15 -0
- data/config/initializers/resque.rb +3 -0
- data/config/resque-pool.yml +6 -0
- data/config/routes.rb +39 -0
- data/config/spring.rb +1 -0
- data/config/vite.json +15 -0
- data/lib/easy_ml/configuration.rb +64 -0
- data/lib/easy_ml/core/evaluators/base_evaluator.rb +53 -0
- data/lib/easy_ml/core/evaluators/classification_evaluators.rb +126 -0
- data/lib/easy_ml/core/evaluators/regression_evaluators.rb +66 -0
- data/lib/easy_ml/core/model_evaluator.rb +161 -89
- data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +28 -18
- data/lib/easy_ml/core/tuner/adapters/xgboost_adapter.rb +4 -25
- data/lib/easy_ml/core/tuner.rb +123 -62
- data/lib/easy_ml/core.rb +0 -3
- data/lib/easy_ml/core_ext/hash.rb +24 -0
- data/lib/easy_ml/core_ext/pathname.rb +11 -5
- data/lib/easy_ml/data/date_converter.rb +90 -0
- data/lib/easy_ml/data/filter_extensions.rb +31 -0
- data/lib/easy_ml/data/polars_column.rb +126 -0
- data/lib/easy_ml/data/polars_reader.rb +297 -0
- data/lib/easy_ml/data/preprocessor.rb +280 -142
- data/lib/easy_ml/data/simple_imputer.rb +255 -0
- data/lib/easy_ml/data/splits/file_split.rb +252 -0
- data/lib/easy_ml/data/splits/in_memory_split.rb +54 -0
- data/lib/easy_ml/data/splits/split.rb +95 -0
- data/lib/easy_ml/data/splits.rb +9 -0
- data/lib/easy_ml/data/statistics_learner.rb +93 -0
- data/lib/easy_ml/data/synced_directory.rb +341 -0
- data/lib/easy_ml/data.rb +6 -2
- data/lib/easy_ml/engine.rb +105 -6
- data/lib/easy_ml/feature_store.rb +227 -0
- data/lib/easy_ml/features.rb +61 -0
- data/lib/easy_ml/initializers/inflections.rb +17 -3
- data/lib/easy_ml/logging.rb +2 -2
- data/lib/easy_ml/predict.rb +74 -0
- data/lib/easy_ml/railtie/generators/migration/migration_generator.rb +192 -36
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_column_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_columns.rb.tt +25 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_dataset_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasets.rb.tt +31 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasource_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasources.rb.tt +16 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_deploys.rb.tt +24 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_events.rb.tt +20 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_feature_histories.rb.tt +14 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_features.rb.tt +32 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_file_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_files.rb.tt +17 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_models.rb.tt +20 -9
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_predictions.rb.tt +17 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_retraining_jobs.rb.tt +77 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_settings.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitter_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitters.rb.tt +15 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_tuner_jobs.rb.tt +40 -0
- data/lib/easy_ml/support/est.rb +5 -1
- data/lib/easy_ml/support/file_rotate.rb +79 -15
- data/lib/easy_ml/support/file_support.rb +9 -0
- data/lib/easy_ml/support/local_file.rb +24 -0
- data/lib/easy_ml/support/lockable.rb +62 -0
- data/lib/easy_ml/support/synced_file.rb +103 -0
- data/lib/easy_ml/support/utc.rb +5 -1
- data/lib/easy_ml/support.rb +6 -3
- data/lib/easy_ml/version.rb +4 -1
- data/lib/easy_ml.rb +7 -2
- metadata +355 -72
- data/app/models/easy_ml/models.rb +0 -5
- data/lib/easy_ml/core/model.rb +0 -30
- data/lib/easy_ml/core/model_core.rb +0 -181
- data/lib/easy_ml/core/models/hyperparameters/base.rb +0 -34
- data/lib/easy_ml/core/models/hyperparameters/xgboost.rb +0 -19
- data/lib/easy_ml/core/models/xgboost.rb +0 -10
- data/lib/easy_ml/core/models/xgboost_core.rb +0 -220
- data/lib/easy_ml/core/models.rb +0 -10
- data/lib/easy_ml/core/uploaders/model_uploader.rb +0 -24
- data/lib/easy_ml/core/uploaders.rb +0 -7
- data/lib/easy_ml/data/dataloader.rb +0 -6
- data/lib/easy_ml/data/dataset/data/preprocessor/statistics.json +0 -31
- data/lib/easy_ml/data/dataset/data/sample_info.json +0 -1
- data/lib/easy_ml/data/dataset/dataset/files/sample_info.json +0 -1
- data/lib/easy_ml/data/dataset/splits/file_split.rb +0 -140
- data/lib/easy_ml/data/dataset/splits/in_memory_split.rb +0 -49
- data/lib/easy_ml/data/dataset/splits/split.rb +0 -98
- data/lib/easy_ml/data/dataset/splits.rb +0 -11
- data/lib/easy_ml/data/dataset/splitters/date_splitter.rb +0 -43
- data/lib/easy_ml/data/dataset/splitters.rb +0 -9
- data/lib/easy_ml/data/dataset.rb +0 -430
- data/lib/easy_ml/data/datasource/datasource_factory.rb +0 -60
- data/lib/easy_ml/data/datasource/file_datasource.rb +0 -40
- data/lib/easy_ml/data/datasource/merged_datasource.rb +0 -64
- data/lib/easy_ml/data/datasource/polars_datasource.rb +0 -41
- data/lib/easy_ml/data/datasource/s3_datasource.rb +0 -89
- data/lib/easy_ml/data/datasource.rb +0 -33
- data/lib/easy_ml/data/preprocessor/preprocessor.rb +0 -205
- data/lib/easy_ml/data/preprocessor/simple_imputer.rb +0 -402
- data/lib/easy_ml/deployment.rb +0 -5
- data/lib/easy_ml/support/synced_directory.rb +0 -134
- data/lib/easy_ml/transforms.rb +0 -29
- /data/{lib/easy_ml/core → app/models/easy_ml}/models/hyperparameters.rb +0 -0
@@ -0,0 +1,147 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { router, usePage } from '@inertiajs/react';
|
3
|
+
import { useInertiaForm } from 'use-inertia-form';
|
4
|
+
import { SearchableSelect } from '../components/SearchableSelect';
|
5
|
+
import type { Datasource, DatasourceFormProps } from '../types/datasource';
|
6
|
+
|
7
|
+
export default function DatasourceFormPage({ datasource, constants }: DatasourceFormProps) {
|
8
|
+
const { rootPath } = usePage().props;
|
9
|
+
const isEditing = !!datasource;
|
10
|
+
|
11
|
+
const { data, setData, processing, errors } = useInertiaForm<{ datasource: Datasource }>({
|
12
|
+
datasource: {
|
13
|
+
name: datasource?.name ?? '',
|
14
|
+
datasource_type: datasource?.datasource_type ?? 's3',
|
15
|
+
s3_bucket: datasource?.s3_bucket ?? '',
|
16
|
+
s3_prefix: datasource?.s3_prefix ?? '',
|
17
|
+
s3_region: datasource?.s3_region ?? 'us-east-1',
|
18
|
+
}
|
19
|
+
});
|
20
|
+
|
21
|
+
const handleSubmit = (e: React.FormEvent) => {
|
22
|
+
e.preventDefault();
|
23
|
+
if (isEditing) {
|
24
|
+
router.patch(`${rootPath}/datasources/${datasource.id}`, data);
|
25
|
+
} else {
|
26
|
+
router.post(`${rootPath}/datasources`, data);
|
27
|
+
}
|
28
|
+
};
|
29
|
+
|
30
|
+
return (
|
31
|
+
<div className="max-w-2xl mx-auto py-8">
|
32
|
+
<div className="bg-white rounded-lg shadow-lg p-6">
|
33
|
+
<h2 className="text-xl font-semibold text-gray-900 mb-6">
|
34
|
+
{isEditing ? 'Edit Datasource' : 'New Datasource'}
|
35
|
+
</h2>
|
36
|
+
|
37
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
38
|
+
<div>
|
39
|
+
<label
|
40
|
+
htmlFor="name"
|
41
|
+
className="block text-sm font-medium text-gray-700"
|
42
|
+
>
|
43
|
+
Name
|
44
|
+
</label>
|
45
|
+
<input
|
46
|
+
type="text"
|
47
|
+
id="name"
|
48
|
+
value={data.datasource.name}
|
49
|
+
onChange={(e) => setData('datasource.name', e.target.value)}
|
50
|
+
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-2 px-4 shadow-sm border-gray-300 border"
|
51
|
+
required
|
52
|
+
/>
|
53
|
+
{errors.datasource?.name && (
|
54
|
+
<p className="mt-1 text-sm text-red-600">{errors.datasource.name}</p>
|
55
|
+
)}
|
56
|
+
</div>
|
57
|
+
|
58
|
+
{!isEditing && (
|
59
|
+
<div>
|
60
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
61
|
+
Type
|
62
|
+
</label>
|
63
|
+
<SearchableSelect
|
64
|
+
options={constants.DATASOURCE_TYPES}
|
65
|
+
value={data.datasource.datasource_type}
|
66
|
+
onChange={(value) => setData('datasource.datasource_type', value)}
|
67
|
+
placeholder="Select datasource type"
|
68
|
+
/>
|
69
|
+
</div>
|
70
|
+
)}
|
71
|
+
|
72
|
+
<div>
|
73
|
+
<label
|
74
|
+
htmlFor="s3_bucket"
|
75
|
+
className="block text-sm font-medium text-gray-700"
|
76
|
+
>
|
77
|
+
S3 Bucket
|
78
|
+
</label>
|
79
|
+
<input
|
80
|
+
type="text"
|
81
|
+
id="s3_bucket"
|
82
|
+
value={data.datasource.s3_bucket}
|
83
|
+
onChange={(e) => setData('datasource.s3_bucket', e.target.value)}
|
84
|
+
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-2 px-4 shadow-sm border-gray-300 border"
|
85
|
+
required
|
86
|
+
/>
|
87
|
+
{errors.datasource?.s3_bucket && (
|
88
|
+
<p className="mt-1 text-sm text-red-600">{errors.datasource.s3_bucket}</p>
|
89
|
+
)}
|
90
|
+
</div>
|
91
|
+
|
92
|
+
<div>
|
93
|
+
<label
|
94
|
+
htmlFor="s3_prefix"
|
95
|
+
className="block text-sm font-medium text-gray-700"
|
96
|
+
>
|
97
|
+
S3 Prefix
|
98
|
+
</label>
|
99
|
+
<input
|
100
|
+
type="text"
|
101
|
+
id="s3_prefix"
|
102
|
+
value={data.datasource.s3_prefix}
|
103
|
+
onChange={(e) => setData('datasource.s3_prefix', e.target.value)}
|
104
|
+
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-2 px-4 shadow-sm border-gray-300 border"
|
105
|
+
placeholder="data/raw/"
|
106
|
+
/>
|
107
|
+
{errors.datasource?.s3_prefix && (
|
108
|
+
<p className="mt-1 text-sm text-red-600">{errors.datasource.s3_prefix}</p>
|
109
|
+
)}
|
110
|
+
</div>
|
111
|
+
|
112
|
+
<div>
|
113
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
114
|
+
S3 Region
|
115
|
+
</label>
|
116
|
+
<SearchableSelect
|
117
|
+
options={constants.s3.S3_REGIONS}
|
118
|
+
value={data.datasource.s3_region}
|
119
|
+
onChange={(value) => setData('datasource.s3_region', value)}
|
120
|
+
placeholder="Select s3 region"
|
121
|
+
/>
|
122
|
+
{errors.datasource?.s3_region && (
|
123
|
+
<p className="mt-1 text-sm text-red-600">{errors.datasource.s3_region}</p>
|
124
|
+
)}
|
125
|
+
</div>
|
126
|
+
|
127
|
+
<div className="flex justify-end gap-3">
|
128
|
+
<button
|
129
|
+
type="button"
|
130
|
+
onClick={() => router.visit(`${rootPath}/datasources`)}
|
131
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900"
|
132
|
+
>
|
133
|
+
Cancel
|
134
|
+
</button>
|
135
|
+
<button
|
136
|
+
type="submit"
|
137
|
+
disabled={processing}
|
138
|
+
className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
139
|
+
>
|
140
|
+
{processing ? 'Saving...' : isEditing ? 'Save Changes' : 'Create Datasource'}
|
141
|
+
</button>
|
142
|
+
</div>
|
143
|
+
</form>
|
144
|
+
</div>
|
145
|
+
</div>
|
146
|
+
);
|
147
|
+
}
|
@@ -0,0 +1,261 @@
|
|
1
|
+
import React, { useState, useMemo, useEffect } from 'react';
|
2
|
+
import { Link, usePage, router } from '@inertiajs/react';
|
3
|
+
import { HardDrive, Plus, Trash2, Settings, RefreshCw, ChevronDown, ChevronUp, AlertCircle } from 'lucide-react';
|
4
|
+
import { EmptyState } from '../components/EmptyState';
|
5
|
+
import { SearchInput } from '../components/SearchInput';
|
6
|
+
import { Pagination } from '../components/Pagination';
|
7
|
+
import type { Datasource } from '../types/datasource';
|
8
|
+
import { Badge } from "@/components/ui/badge";
|
9
|
+
|
10
|
+
const ITEMS_PER_PAGE = 6;
|
11
|
+
|
12
|
+
export default function DatasourcesPage({ datasources }: { datasources: Datasource[] }) {
|
13
|
+
const { rootPath } = usePage().props;
|
14
|
+
const [searchQuery, setSearchQuery] = useState('');
|
15
|
+
const [currentPage, setCurrentPage] = useState(1);
|
16
|
+
const [expandedErrors, setExpandedErrors] = useState<number[]>([]);
|
17
|
+
|
18
|
+
const filteredDatasources = useMemo(() => {
|
19
|
+
return datasources.filter(source =>
|
20
|
+
source.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
21
|
+
source.s3_bucket.toLowerCase().includes(searchQuery.toLowerCase())
|
22
|
+
);
|
23
|
+
}, [searchQuery, datasources]);
|
24
|
+
|
25
|
+
const totalPages = Math.ceil(filteredDatasources.length / ITEMS_PER_PAGE);
|
26
|
+
const paginatedDatasources = filteredDatasources.slice(
|
27
|
+
(currentPage - 1) * ITEMS_PER_PAGE,
|
28
|
+
currentPage * ITEMS_PER_PAGE
|
29
|
+
);
|
30
|
+
|
31
|
+
const handleDelete = (datasourceId: number) => {
|
32
|
+
if (confirm('Are you sure you want to delete this datasource? This action cannot be undone.')) {
|
33
|
+
router.delete(`${rootPath}/datasources/${datasourceId}`);
|
34
|
+
}
|
35
|
+
};
|
36
|
+
|
37
|
+
const toggleError = (id: number) => {
|
38
|
+
setExpandedErrors(prev =>
|
39
|
+
prev.includes(id)
|
40
|
+
? prev.filter(expandedId => expandedId !== id)
|
41
|
+
: [...prev, id]
|
42
|
+
);
|
43
|
+
};
|
44
|
+
|
45
|
+
const handleSync = async (id: number) => {
|
46
|
+
try {
|
47
|
+
router.post(`${rootPath}/datasources/${id}/sync`, {}, {
|
48
|
+
preserveScroll: true, // Keeps the scroll position
|
49
|
+
preserveState: true, // Keeps the form state
|
50
|
+
onSuccess: () => {
|
51
|
+
// The page will automatically refresh with new data
|
52
|
+
},
|
53
|
+
onError: () => {
|
54
|
+
// Handle error case if needed
|
55
|
+
console.error('Failed to sync datasource');
|
56
|
+
}
|
57
|
+
});
|
58
|
+
} catch (error) {
|
59
|
+
console.error('Failed to sync datasource:', error);
|
60
|
+
}
|
61
|
+
};
|
62
|
+
|
63
|
+
const formatLastSyncedAt = (lastSyncedAt: string) => {
|
64
|
+
if (lastSyncedAt === 'Not Synced') return lastSyncedAt;
|
65
|
+
|
66
|
+
const date = new Date(lastSyncedAt);
|
67
|
+
return isNaN(date.getTime()) ? lastSyncedAt : date.toLocaleString();
|
68
|
+
};
|
69
|
+
|
70
|
+
useEffect(() => {
|
71
|
+
let pollInterval: number | undefined;
|
72
|
+
|
73
|
+
// Check if any datasource is syncing
|
74
|
+
const isAnySyncing = datasources.some(d => d.is_syncing);
|
75
|
+
|
76
|
+
if (isAnySyncing) {
|
77
|
+
// Start polling every 5 seconds
|
78
|
+
pollInterval = window.setInterval(() => {
|
79
|
+
router.get(window.location.href, {}, {
|
80
|
+
preserveScroll: true,
|
81
|
+
preserveState: true,
|
82
|
+
only: ['datasources']
|
83
|
+
});
|
84
|
+
}, 2000);
|
85
|
+
}
|
86
|
+
|
87
|
+
// Cleanup function
|
88
|
+
return () => {
|
89
|
+
if (pollInterval) {
|
90
|
+
window.clearInterval(pollInterval);
|
91
|
+
}
|
92
|
+
};
|
93
|
+
}, [datasources]);
|
94
|
+
|
95
|
+
if (datasources.length === 0) {
|
96
|
+
return (
|
97
|
+
<div className="p-8">
|
98
|
+
<EmptyState
|
99
|
+
icon={HardDrive}
|
100
|
+
title="Connect your first data source"
|
101
|
+
description="Connect to your data sources to start creating datasets and training models"
|
102
|
+
actionLabel="Add Datasource"
|
103
|
+
onAction={() => { router.visit(`${rootPath}/datasources/new`) }}
|
104
|
+
/>
|
105
|
+
</div>
|
106
|
+
);
|
107
|
+
}
|
108
|
+
|
109
|
+
return (
|
110
|
+
<div className="p-8">
|
111
|
+
<div className="space-y-6">
|
112
|
+
<div className="flex justify-between items-center">
|
113
|
+
<div className="flex items-center gap-4">
|
114
|
+
<h2 className="text-xl font-semibold text-gray-900">Datasources</h2>
|
115
|
+
<SearchInput
|
116
|
+
value={searchQuery}
|
117
|
+
onChange={setSearchQuery}
|
118
|
+
placeholder="Search datasources..."
|
119
|
+
/>
|
120
|
+
</div>
|
121
|
+
<Link
|
122
|
+
href={`${rootPath}/datasources/new`}
|
123
|
+
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
124
|
+
>
|
125
|
+
<Plus className="w-4 h-4" />
|
126
|
+
New Datasource
|
127
|
+
</Link>
|
128
|
+
</div>
|
129
|
+
|
130
|
+
{paginatedDatasources.length === 0 ? (
|
131
|
+
<div className="text-center py-12 bg-white rounded-lg shadow">
|
132
|
+
<HardDrive className="mx-auto h-12 w-12 text-gray-400" />
|
133
|
+
<h3 className="mt-2 text-sm font-medium text-gray-900">No datasources found</h3>
|
134
|
+
<p className="mt-1 text-sm text-gray-500">
|
135
|
+
No datasources match your search criteria. Try adjusting your search or add a new datasource.
|
136
|
+
</p>
|
137
|
+
<div className="mt-6">
|
138
|
+
<Link
|
139
|
+
href={`${rootPath}/datasources/new`}
|
140
|
+
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
141
|
+
>
|
142
|
+
<Plus className="w-4 h-4 mr-2" />
|
143
|
+
New Datasource
|
144
|
+
</Link>
|
145
|
+
</div>
|
146
|
+
</div>
|
147
|
+
) : (
|
148
|
+
<>
|
149
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
150
|
+
{paginatedDatasources.map((datasource) => (
|
151
|
+
<div
|
152
|
+
key={datasource.id}
|
153
|
+
className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow"
|
154
|
+
>
|
155
|
+
<div className="flex justify-between items-start mb-4">
|
156
|
+
<div className="flex items-start gap-3">
|
157
|
+
<HardDrive className="w-5 h-5 text-blue-600 mt-1" />
|
158
|
+
<div>
|
159
|
+
<div className="flex items-center gap-2">
|
160
|
+
<h3 className="text-lg font-semibold text-gray-900">
|
161
|
+
{datasource.name}
|
162
|
+
</h3>
|
163
|
+
{datasource.is_syncing ? (
|
164
|
+
<Badge variant="warning">syncing</Badge>
|
165
|
+
) : datasource.sync_error ? (
|
166
|
+
<Badge variant="important">sync error</Badge>
|
167
|
+
) : datasource.last_synced_at !== 'Not Synced' ? (
|
168
|
+
<Badge variant="success">synced</Badge>
|
169
|
+
) : (
|
170
|
+
<Badge variant="warning">not synced</Badge>
|
171
|
+
)}
|
172
|
+
</div>
|
173
|
+
<p className="text-sm text-gray-500 mt-1">
|
174
|
+
s3://{datasource.s3_bucket}/{datasource.s3_prefix}
|
175
|
+
</p>
|
176
|
+
</div>
|
177
|
+
</div>
|
178
|
+
<div className="flex gap-2">
|
179
|
+
<button
|
180
|
+
onClick={() => handleSync(datasource.id)}
|
181
|
+
disabled={datasource.is_syncing}
|
182
|
+
className={`text-gray-400 hover:text-blue-600 transition-colors ${
|
183
|
+
datasource.is_syncing ? 'animate-spin' : ''
|
184
|
+
}`}
|
185
|
+
title="Sync datasource"
|
186
|
+
>
|
187
|
+
<RefreshCw className="w-5 h-5" />
|
188
|
+
</button>
|
189
|
+
<Link
|
190
|
+
href={`${rootPath}/datasources/${datasource.id}/edit`}
|
191
|
+
className="text-gray-400 hover:text-blue-600 transition-colors"
|
192
|
+
title="Edit datasource"
|
193
|
+
>
|
194
|
+
<Settings className="w-5 h-5" />
|
195
|
+
</Link>
|
196
|
+
<button
|
197
|
+
onClick={() => handleDelete(datasource.id)}
|
198
|
+
className="text-gray-400 hover:text-red-600 transition-colors"
|
199
|
+
title="Delete datasource"
|
200
|
+
>
|
201
|
+
<Trash2 className="w-5 h-5" />
|
202
|
+
</button>
|
203
|
+
</div>
|
204
|
+
</div>
|
205
|
+
|
206
|
+
<div className="grid grid-cols-2 gap-4 mt-4">
|
207
|
+
<div>
|
208
|
+
<span className="text-sm text-gray-500">Region</span>
|
209
|
+
<p className="text-sm font-medium text-gray-900">
|
210
|
+
{datasource.s3_region}
|
211
|
+
</p>
|
212
|
+
</div>
|
213
|
+
<div>
|
214
|
+
<span className="text-sm text-gray-500">Last Sync</span>
|
215
|
+
<p className="text-sm font-medium text-gray-900">
|
216
|
+
{formatLastSyncedAt(datasource.last_synced_at)}
|
217
|
+
</p>
|
218
|
+
</div>
|
219
|
+
</div>
|
220
|
+
|
221
|
+
{datasource.sync_error && datasource.stacktrace && (
|
222
|
+
<div className="mt-4 pt-4 border-t border-gray-100">
|
223
|
+
<button
|
224
|
+
onClick={() => toggleError(datasource.id)}
|
225
|
+
className="flex items-center gap-2 text-sm text-red-600 hover:text-red-700"
|
226
|
+
>
|
227
|
+
<AlertCircle className="w-4 h-4" />
|
228
|
+
<span>View Error Details</span>
|
229
|
+
{expandedErrors.includes(datasource.id) ? (
|
230
|
+
<ChevronUp className="w-4 h-4" />
|
231
|
+
) : (
|
232
|
+
<ChevronDown className="w-4 h-4" />
|
233
|
+
)}
|
234
|
+
</button>
|
235
|
+
{expandedErrors.includes(datasource.id) && (
|
236
|
+
<div className="mt-2 p-3 bg-red-50 rounded-md">
|
237
|
+
<pre className="text-xs text-red-700 whitespace-pre-wrap font-mono">
|
238
|
+
{datasource.stacktrace}
|
239
|
+
</pre>
|
240
|
+
</div>
|
241
|
+
)}
|
242
|
+
</div>
|
243
|
+
)}
|
244
|
+
|
245
|
+
</div>
|
246
|
+
))}
|
247
|
+
</div>
|
248
|
+
|
249
|
+
{totalPages > 1 && (
|
250
|
+
<Pagination
|
251
|
+
currentPage={currentPage}
|
252
|
+
totalPages={totalPages}
|
253
|
+
onPageChange={setCurrentPage}
|
254
|
+
/>
|
255
|
+
)}
|
256
|
+
</>
|
257
|
+
)}
|
258
|
+
</div>
|
259
|
+
</div>
|
260
|
+
);
|
261
|
+
}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { router, Link, usePage } from "@inertiajs/react";
|
3
|
+
import { useInertiaForm } from "use-inertia-form";
|
4
|
+
import { ArrowLeft, Brain } from 'lucide-react';
|
5
|
+
import { ModelForm } from '../components/ModelForm';
|
6
|
+
import { Model, Dataset } from '../types';
|
7
|
+
|
8
|
+
interface ModelFormData {
|
9
|
+
model: Model;
|
10
|
+
}
|
11
|
+
|
12
|
+
interface PageProps {
|
13
|
+
model: Model;
|
14
|
+
datasets: Dataset[];
|
15
|
+
constants: {
|
16
|
+
modelTypes: string[];
|
17
|
+
tasks: string[];
|
18
|
+
objectives: string[];
|
19
|
+
metrics: string[];
|
20
|
+
};
|
21
|
+
}
|
22
|
+
|
23
|
+
export default function EditModelPage({ model, datasets, constants }: PageProps) {
|
24
|
+
return (
|
25
|
+
<div className="max-w-3xl mx-auto py-8">
|
26
|
+
<div className="bg-white rounded-lg shadow-lg">
|
27
|
+
<div className="px-6 py-4 border-b border-gray-200">
|
28
|
+
<div className="flex items-center gap-3">
|
29
|
+
<Brain className="w-6 h-6 text-blue-600" />
|
30
|
+
<h2 className="text-xl font-semibold text-gray-900">Edit Model</h2>
|
31
|
+
</div>
|
32
|
+
</div>
|
33
|
+
|
34
|
+
<div className="p-6">
|
35
|
+
<ModelForm
|
36
|
+
initialData={model}
|
37
|
+
datasets={datasets}
|
38
|
+
constants={constants}
|
39
|
+
isEditing={true}
|
40
|
+
/>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
</div>
|
44
|
+
);
|
45
|
+
}
|
@@ -0,0 +1,56 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
// import { useNavigate, useParams } from 'react-router-dom';
|
3
|
+
import { Code2 } from 'lucide-react';
|
4
|
+
import { mockDatasets, mockFeatureGroups } from '../mockData';
|
5
|
+
import { FeatureForm } from '../components/features/FeatureForm';
|
6
|
+
|
7
|
+
export default function EditFeaturePage() {
|
8
|
+
const navigate = useNavigate();
|
9
|
+
const { id } = useParams();
|
10
|
+
|
11
|
+
const feature = mockFeatureGroups
|
12
|
+
.flatMap(g => g.features)
|
13
|
+
.find(t => t.id === Number(id));
|
14
|
+
|
15
|
+
if (!feature) {
|
16
|
+
return (
|
17
|
+
<div className="text-center py-12">
|
18
|
+
<h2 className="text-xl font-semibold text-gray-900">Feature not found</h2>
|
19
|
+
</div>
|
20
|
+
);
|
21
|
+
}
|
22
|
+
|
23
|
+
const handleSubmit = (data: any) => {
|
24
|
+
console.log('Updating feature:', data);
|
25
|
+
navigate('/features');
|
26
|
+
};
|
27
|
+
|
28
|
+
return (
|
29
|
+
<div className="max-w-4xl mx-auto p-8">
|
30
|
+
<div className="bg-white rounded-lg shadow-lg">
|
31
|
+
<div className="px-6 py-4 border-b border-gray-200">
|
32
|
+
<div className="flex items-center gap-3">
|
33
|
+
<Code2 className="w-6 h-6 text-blue-600" />
|
34
|
+
<h2 className="text-xl font-semibold text-gray-900">Edit Feature</h2>
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
|
38
|
+
<FeatureForm
|
39
|
+
datasets={mockDatasets}
|
40
|
+
groups={mockFeatureGroups}
|
41
|
+
initialData={{
|
42
|
+
name: feature.name,
|
43
|
+
description: feature.description,
|
44
|
+
groupId: feature.groupId,
|
45
|
+
testDatasetId: feature.testDatasetId,
|
46
|
+
inputColumns: feature.inputColumns,
|
47
|
+
outputColumns: feature.outputColumns,
|
48
|
+
code: feature.code
|
49
|
+
}}
|
50
|
+
onSubmit={handleSubmit}
|
51
|
+
onCancel={() => navigate('/features')}
|
52
|
+
/>
|
53
|
+
</div>
|
54
|
+
</div>
|
55
|
+
);
|
56
|
+
}
|
@@ -0,0 +1,115 @@
|
|
1
|
+
import React, { useState, useMemo, useEffect } from 'react';
|
2
|
+
import { Brain, Plus, Trash2 } from 'lucide-react';
|
3
|
+
import { ModelCard } from '../components/ModelCard';
|
4
|
+
import { EmptyState } from '../components/EmptyState';
|
5
|
+
import { SearchInput } from '../components/SearchInput';
|
6
|
+
import { Pagination } from '../components/Pagination';
|
7
|
+
import { router } from '@inertiajs/react';
|
8
|
+
|
9
|
+
const ITEMS_PER_PAGE = 6;
|
10
|
+
|
11
|
+
export default function ModelsPage({ rootPath, models }) {
|
12
|
+
const [selectedModelId, setSelectedModelId] = useState<number | null>(null);
|
13
|
+
const [searchQuery, setSearchQuery] = useState('');
|
14
|
+
const [currentPage, setCurrentPage] = useState(1);
|
15
|
+
|
16
|
+
const filteredModels = useMemo(() => {
|
17
|
+
return models.filter(model =>
|
18
|
+
model.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
19
|
+
model.model_type.toLowerCase().includes(searchQuery.toLowerCase())
|
20
|
+
);
|
21
|
+
}, [searchQuery, models]);
|
22
|
+
|
23
|
+
const totalPages = Math.ceil(filteredModels.length / ITEMS_PER_PAGE);
|
24
|
+
const paginatedModels = filteredModels.slice(
|
25
|
+
(currentPage - 1) * ITEMS_PER_PAGE,
|
26
|
+
currentPage * ITEMS_PER_PAGE
|
27
|
+
);
|
28
|
+
|
29
|
+
const handleDelete = (modelId: number) => {
|
30
|
+
if (confirm('Are you sure you want to delete this model?')) {
|
31
|
+
router.delete(`${rootPath}/models/${modelId}`);
|
32
|
+
}
|
33
|
+
};
|
34
|
+
|
35
|
+
if (models.length === 0) {
|
36
|
+
return (
|
37
|
+
<div className="p-8">
|
38
|
+
<EmptyState
|
39
|
+
icon={Brain}
|
40
|
+
title="Create your first ML model"
|
41
|
+
description="Get started by creating a machine learning model. You can train models for classification, regression, and more."
|
42
|
+
actionLabel="Create Model"
|
43
|
+
onAction={() => {
|
44
|
+
router.visit(`${rootPath}/models/new`)
|
45
|
+
}}
|
46
|
+
/>
|
47
|
+
</div>
|
48
|
+
);
|
49
|
+
}
|
50
|
+
|
51
|
+
return (
|
52
|
+
<div className="p-8">
|
53
|
+
<div className="space-y-6">
|
54
|
+
<div className="flex justify-between items-center">
|
55
|
+
<div className="flex items-center gap-4">
|
56
|
+
<h2 className="text-xl font-semibold text-gray-900">Models</h2>
|
57
|
+
<SearchInput
|
58
|
+
value={searchQuery}
|
59
|
+
onChange={setSearchQuery}
|
60
|
+
placeholder="Search models..."
|
61
|
+
/>
|
62
|
+
</div>
|
63
|
+
<button
|
64
|
+
onClick={() => router.visit(`${rootPath}/models/new`)}
|
65
|
+
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
66
|
+
>
|
67
|
+
<Plus className="w-4 h-4" />
|
68
|
+
New Model
|
69
|
+
</button>
|
70
|
+
</div>
|
71
|
+
|
72
|
+
{paginatedModels.length === 0 ? (
|
73
|
+
<div className="text-center py-12 bg-white rounded-lg shadow">
|
74
|
+
<Brain className="mx-auto h-12 w-12 text-gray-400" />
|
75
|
+
<h3 className="mt-2 text-sm font-medium text-gray-900">No models found</h3>
|
76
|
+
<p className="mt-1 text-sm text-gray-500">
|
77
|
+
No models match your search criteria. Try adjusting your search or create a new model.
|
78
|
+
</p>
|
79
|
+
<div className="mt-6">
|
80
|
+
<button
|
81
|
+
onClick={() => router.visit(`${rootPath}/models/new`)}
|
82
|
+
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
83
|
+
>
|
84
|
+
<Plus className="w-4 h-4 mr-2" />
|
85
|
+
New Model
|
86
|
+
</button>
|
87
|
+
</div>
|
88
|
+
</div>
|
89
|
+
) : (
|
90
|
+
<>
|
91
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
92
|
+
{paginatedModels.map((model) => (
|
93
|
+
<ModelCard
|
94
|
+
rootPath={rootPath}
|
95
|
+
key={model.id}
|
96
|
+
initialModel={model}
|
97
|
+
onViewDetails={setSelectedModelId}
|
98
|
+
handleDelete={handleDelete}
|
99
|
+
/>
|
100
|
+
))}
|
101
|
+
</div>
|
102
|
+
|
103
|
+
{totalPages > 1 && (
|
104
|
+
<Pagination
|
105
|
+
currentPage={currentPage}
|
106
|
+
totalPages={totalPages}
|
107
|
+
onPageChange={setCurrentPage}
|
108
|
+
/>
|
109
|
+
)}
|
110
|
+
</>
|
111
|
+
)}
|
112
|
+
</div>
|
113
|
+
</div>
|
114
|
+
);
|
115
|
+
}
|