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,88 @@
|
|
1
|
+
# == Schema Information
|
2
|
+
#
|
3
|
+
# Table name: easy_ml_datasources
|
4
|
+
#
|
5
|
+
# id :bigint not null, primary key
|
6
|
+
# name :string not null
|
7
|
+
# datasource_type :string
|
8
|
+
# root_dir :string
|
9
|
+
# configuration :json
|
10
|
+
# created_at :datetime not null
|
11
|
+
# updated_at :datetime not null
|
12
|
+
#
|
13
|
+
module EasyML
|
14
|
+
class DatasourcesController < ApplicationController
|
15
|
+
def index
|
16
|
+
@datasources = Datasource.all.order(id: :asc)
|
17
|
+
render inertia: "pages/DatasourcesPage", props: {
|
18
|
+
datasources: @datasources.map { |datasource| datasource_to_json(datasource) },
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def show
|
23
|
+
@datasource = Datasource.find(params[:id])
|
24
|
+
render json: @datasource, serializer: DatasourceSerializer
|
25
|
+
end
|
26
|
+
|
27
|
+
def edit
|
28
|
+
datasource = EasyML::Datasource.find_by(id: params[:id])
|
29
|
+
|
30
|
+
render inertia: "pages/DatasourceFormPage", props: {
|
31
|
+
datasource: datasource_to_json(datasource),
|
32
|
+
constants: EasyML::Datasource.constants,
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def new
|
37
|
+
render inertia: "pages/DatasourceFormPage", props: {
|
38
|
+
constants: EasyML::Datasource.constants,
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def create
|
43
|
+
EasyML::Datasource.transaction do
|
44
|
+
datasource = EasyML::Datasource.create!(datasource_params)
|
45
|
+
end
|
46
|
+
|
47
|
+
redirect_to easy_ml_datasources_path, notice: "Datasource was successfully created."
|
48
|
+
rescue ActiveRecord::RecordInvalid => e
|
49
|
+
redirect_to new_easy_ml_datasource_path, alert: e.record.errors.full_messages.join(", ")
|
50
|
+
end
|
51
|
+
|
52
|
+
def destroy
|
53
|
+
@datasource = Datasource.find(params[:id])
|
54
|
+
@datasource.destroy
|
55
|
+
|
56
|
+
redirect_to easy_ml_datasources_path, notice: "Datasource was successfully deleted."
|
57
|
+
end
|
58
|
+
|
59
|
+
def update
|
60
|
+
datasource = EasyML::Datasource.find(params[:id])
|
61
|
+
if datasource.update(datasource_params)
|
62
|
+
redirect_to easy_ml_datasources_path, notice: "Datasource was successfully updated."
|
63
|
+
else
|
64
|
+
redirect_to edit_easy_ml_datasource_path(datasource), alert: datasource.errors.full_messages.join(", ")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def sync
|
69
|
+
datasource = Datasource.find(params[:id])
|
70
|
+
datasource.update(is_syncing: true)
|
71
|
+
|
72
|
+
# Start sync in background to avoid blocking
|
73
|
+
datasource.refresh_async
|
74
|
+
|
75
|
+
redirect_to easy_ml_datasources_path, notice: "Datasource is syncing..."
|
76
|
+
rescue ActiveRecord::RecordNotFound
|
77
|
+
redirect_to easy_ml_datasources_path, error: "Datasource not found..."
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def datasource_params
|
83
|
+
params.require(:datasource).permit(:name, :s3_bucket, :s3_prefix, :s3_region, :datasource_type).merge!(
|
84
|
+
datasource_type: "s3",
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module EasyML
|
2
|
+
class DeploysController < ApplicationController
|
3
|
+
def create
|
4
|
+
run = EasyML::RetrainingRun.find(params[:retraining_run_id])
|
5
|
+
run.update(is_deploying: true)
|
6
|
+
@deploy = EasyML::Deploy.create!(
|
7
|
+
model_id: params[:easy_ml_model_id],
|
8
|
+
retraining_run_id: params[:retraining_run_id],
|
9
|
+
trigger: "manual",
|
10
|
+
)
|
11
|
+
@deploy.deploy
|
12
|
+
|
13
|
+
flash[:notice] = "Model deployment has started"
|
14
|
+
redirect_to easy_ml_model_path(@deploy.model)
|
15
|
+
rescue => e
|
16
|
+
flash[:alert] = "Trouble deploying model: #{e.message}"
|
17
|
+
redirect_to easy_ml_model_path(@deploy.model)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# == Schema Information
|
2
|
+
#
|
3
|
+
# Table name: easy_ml_models
|
4
|
+
#
|
5
|
+
# id :bigint not null, primary key
|
6
|
+
# name :string not null
|
7
|
+
# model_type :string
|
8
|
+
# status :string
|
9
|
+
# dataset_id :bigint
|
10
|
+
# configuration :json
|
11
|
+
# version :string not null
|
12
|
+
# root_dir :string
|
13
|
+
# file :json
|
14
|
+
# created_at :datetime not null
|
15
|
+
# updated_at :datetime not null
|
16
|
+
#
|
17
|
+
module EasyML
|
18
|
+
class ModelsController < ApplicationController
|
19
|
+
include EasyML::Engine.routes.url_helpers
|
20
|
+
|
21
|
+
def index
|
22
|
+
models = EasyML::Model.all.includes(includes_list).order(:last_trained_at, :id)
|
23
|
+
|
24
|
+
render inertia: "pages/ModelsPage", props: {
|
25
|
+
models: models.map { |model| model_to_json(model) },
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def new
|
30
|
+
render inertia: "pages/NewModelPage", props: {
|
31
|
+
datasets: EasyML::Dataset.all.map do |dataset|
|
32
|
+
dataset.slice(:id, :name, :num_rows)
|
33
|
+
end,
|
34
|
+
constants: EasyML::Model.constants,
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def edit
|
39
|
+
model = Model.includes(includes_list).find(params[:id])
|
40
|
+
render inertia: "pages/EditModelPage", props: {
|
41
|
+
model: model_to_json(model),
|
42
|
+
datasets: EasyML::Dataset.all.map do |dataset|
|
43
|
+
dataset.slice(:id, :name, :num_rows)
|
44
|
+
end,
|
45
|
+
constants: EasyML::Model.constants,
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def create
|
50
|
+
model = Model.new(model_params)
|
51
|
+
|
52
|
+
if model.save
|
53
|
+
flash[:notice] = "Model was successfully created."
|
54
|
+
redirect_to easy_ml_models_path
|
55
|
+
else
|
56
|
+
render inertia: "pages/NewModelPage", props: {
|
57
|
+
datasets: EasyML::Dataset.all.map do |dataset|
|
58
|
+
dataset.slice(:id, :name, :num_rows)
|
59
|
+
end,
|
60
|
+
constants: EasyML::Model.constants,
|
61
|
+
errors: model.errors.to_hash(true),
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def update
|
67
|
+
model = Model.find(params[:id])
|
68
|
+
|
69
|
+
if model.update(model_params)
|
70
|
+
flash[:notice] = "Model was successfully updated."
|
71
|
+
redirect_to easy_ml_models_path
|
72
|
+
else
|
73
|
+
render inertia: "pages/EditModelPage", props: {
|
74
|
+
model: model_to_json(model),
|
75
|
+
datasets: EasyML::Dataset.all.map { |dataset| dataset_to_json(dataset) },
|
76
|
+
constants: EasyML::Model.constants,
|
77
|
+
errors: model.errors.to_hash(true),
|
78
|
+
}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def show
|
83
|
+
model = Model.includes(includes_list)
|
84
|
+
.find(params[:id])
|
85
|
+
|
86
|
+
if request.format.json?
|
87
|
+
render json: { model: model_to_json(model) }
|
88
|
+
else
|
89
|
+
render inertia: "pages/ShowModelPage", props: {
|
90
|
+
model: model_to_json(model),
|
91
|
+
}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def destroy
|
96
|
+
model = Model.find(params[:id])
|
97
|
+
|
98
|
+
if model.destroy
|
99
|
+
flash[:notice] = "Model was successfully deleted."
|
100
|
+
redirect_to easy_ml_models_path
|
101
|
+
else
|
102
|
+
flash[:alert] = "Failed to delete the model."
|
103
|
+
redirect_to easy_ml_models_path
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def train
|
108
|
+
model = EasyML::Model.find(params[:id])
|
109
|
+
model.train
|
110
|
+
flash[:notice] = "Model training started!"
|
111
|
+
|
112
|
+
redirect_to easy_ml_models_path
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def includes_list
|
118
|
+
[:retraining_runs, :retraining_job, dataset: [:columns, :features, :splitter]]
|
119
|
+
end
|
120
|
+
|
121
|
+
def model_params
|
122
|
+
params.require(:model).permit(
|
123
|
+
:name,
|
124
|
+
:model_type,
|
125
|
+
:dataset_id,
|
126
|
+
:task,
|
127
|
+
:objective,
|
128
|
+
metrics: [],
|
129
|
+
retraining_job_attributes: [
|
130
|
+
:id,
|
131
|
+
:frequency,
|
132
|
+
:active,
|
133
|
+
:metric,
|
134
|
+
:direction,
|
135
|
+
:threshold,
|
136
|
+
:tuning_frequency,
|
137
|
+
:batch_mode,
|
138
|
+
:batch_size,
|
139
|
+
:batch_overlap,
|
140
|
+
:batch_key,
|
141
|
+
:tuning_enabled,
|
142
|
+
at: [:hour, :day_of_week, :day_of_month],
|
143
|
+
tuner_config: [
|
144
|
+
:n_trials,
|
145
|
+
config: {},
|
146
|
+
],
|
147
|
+
],
|
148
|
+
)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module EasyML
|
2
|
+
class RetrainingRunsController < ApplicationController
|
3
|
+
def index
|
4
|
+
model = EasyML::Model.find(params[:id])
|
5
|
+
limit = (params[:limit] || 20).to_i
|
6
|
+
offset = (params[:offset] || 0).to_i
|
7
|
+
|
8
|
+
render json: ModelSerializer.new(
|
9
|
+
model,
|
10
|
+
params: { limit: limit, offset: offset },
|
11
|
+
).serializable_hash
|
12
|
+
end
|
13
|
+
|
14
|
+
def show
|
15
|
+
run = EasyML::RetrainingRun.find(params[:id])
|
16
|
+
render json: RetrainingRunSerializer.new(run).serializable_hash
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# == Schema Information
|
2
|
+
#
|
3
|
+
# Table name: easy_ml_settings
|
4
|
+
#
|
5
|
+
# id :bigint not null, primary key
|
6
|
+
# storage :string
|
7
|
+
# timezone :string
|
8
|
+
# s3_access_key_id :string
|
9
|
+
# s3_secret_access_key :string
|
10
|
+
# s3_bucket :string
|
11
|
+
# s3_region :string
|
12
|
+
# s3_prefix :string
|
13
|
+
# created_at :datetime not null
|
14
|
+
# updated_at :datetime not null
|
15
|
+
#
|
16
|
+
module EasyML
|
17
|
+
class SettingsController < ApplicationController
|
18
|
+
def index
|
19
|
+
@settings = Settings.first_or_create
|
20
|
+
render inertia: "pages/SettingsPage", props: {
|
21
|
+
settings: { settings: settings_to_json(@settings) },
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def update
|
26
|
+
@settings = Settings.first_or_create
|
27
|
+
|
28
|
+
@settings.update(settings_params)
|
29
|
+
EasyML::Configuration.configure do |config|
|
30
|
+
config.storage = @settings.storage
|
31
|
+
config.timezone = @settings.timezone
|
32
|
+
config.s3_access_key_id = @settings.s3_access_key_id
|
33
|
+
config.s3_secret_access_key = @settings.s3_secret_access_key
|
34
|
+
config.s3_bucket = @settings.s3_bucket
|
35
|
+
config.s3_region = @settings.s3_region
|
36
|
+
config.s3_prefix = @settings.s3_prefix
|
37
|
+
end
|
38
|
+
flash.now[:notice] = "Settings saved."
|
39
|
+
render inertia: "pages/SettingsPage", props: {
|
40
|
+
settings: @settings.as_json,
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def settings_params
|
47
|
+
params.require(:settings).permit(
|
48
|
+
:storage,
|
49
|
+
:timezone,
|
50
|
+
:s3_access_key_id,
|
51
|
+
:s3_secret_access_key,
|
52
|
+
:s3_bucket,
|
53
|
+
:s3_region,
|
54
|
+
:s3_prefix,
|
55
|
+
:wandb_api_key
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
import React, { createContext, useContext, useState, useCallback } from 'react';
|
2
|
+
import { AlertCircle, CheckCircle, XCircle, X } from 'lucide-react';
|
3
|
+
|
4
|
+
type AlertType = 'success' | 'error' | 'info';
|
5
|
+
|
6
|
+
interface Alert {
|
7
|
+
id: string;
|
8
|
+
type: AlertType;
|
9
|
+
message: string;
|
10
|
+
isLeaving?: boolean;
|
11
|
+
}
|
12
|
+
|
13
|
+
interface AlertContextType {
|
14
|
+
alerts: Alert[];
|
15
|
+
showAlert: (type: AlertType, message: string) => void;
|
16
|
+
removeAlert: (id: string) => void;
|
17
|
+
}
|
18
|
+
|
19
|
+
const AlertContext = createContext<AlertContextType | undefined>(undefined);
|
20
|
+
|
21
|
+
export function AlertProvider({ children }: { children: React.ReactNode }) {
|
22
|
+
const [alerts, setAlerts] = useState<Alert[]>([]);
|
23
|
+
let numSeconds = 1.25;
|
24
|
+
|
25
|
+
const removeAlert = useCallback((id: string) => {
|
26
|
+
setAlerts(prev =>
|
27
|
+
prev.map(alert =>
|
28
|
+
alert.id === id ? { ...alert, isLeaving: true } : alert
|
29
|
+
)
|
30
|
+
);
|
31
|
+
|
32
|
+
setTimeout(() => {
|
33
|
+
setAlerts(prev => prev.filter(alert => alert.id !== id));
|
34
|
+
}, 300);
|
35
|
+
}, []);
|
36
|
+
|
37
|
+
const showAlert = useCallback((type: AlertType, message: string) => {
|
38
|
+
const id = Math.random().toString(36).substring(7);
|
39
|
+
setAlerts(prev => [...prev, { id, type, message }]);
|
40
|
+
|
41
|
+
setTimeout(() => {
|
42
|
+
removeAlert(id);
|
43
|
+
}, numSeconds * 1000);
|
44
|
+
}, [removeAlert]);
|
45
|
+
|
46
|
+
return (
|
47
|
+
<AlertContext.Provider value={{ alerts, showAlert, removeAlert }}>
|
48
|
+
{children}
|
49
|
+
</AlertContext.Provider>
|
50
|
+
);
|
51
|
+
}
|
52
|
+
|
53
|
+
export function useAlerts() {
|
54
|
+
const context = useContext(AlertContext);
|
55
|
+
if (context === undefined) {
|
56
|
+
throw new Error('useAlerts must be used within an AlertProvider');
|
57
|
+
}
|
58
|
+
return context;
|
59
|
+
}
|
60
|
+
|
61
|
+
export function AlertContainer() {
|
62
|
+
const { alerts, removeAlert } = useAlerts();
|
63
|
+
|
64
|
+
if (alerts.length === 0) return null;
|
65
|
+
|
66
|
+
return (
|
67
|
+
<div className="fixed top-4 right-4 left-4 z-50 flex flex-col gap-2">
|
68
|
+
{alerts.map(alert => (
|
69
|
+
<div
|
70
|
+
key={alert.id}
|
71
|
+
className={`flex items-center justify-between p-4 rounded-lg shadow-lg
|
72
|
+
transition-all duration-300 ease-in-out
|
73
|
+
${alert.isLeaving ? 'opacity-0 feature -translate-y-2' : 'opacity-100'}
|
74
|
+
${
|
75
|
+
alert.type === 'success' ? 'bg-green-50 text-green-900' :
|
76
|
+
alert.type === 'error' ? 'bg-red-50 text-red-900' :
|
77
|
+
'bg-blue-50 text-blue-900'
|
78
|
+
}`}
|
79
|
+
>
|
80
|
+
<div className="flex items-center gap-3">
|
81
|
+
{alert.type === 'success' ? (
|
82
|
+
<CheckCircle className={`w-5 h-5 ${
|
83
|
+
alert.type === 'success' ? 'text-green-500' :
|
84
|
+
alert.type === 'error' ? 'text-red-500' :
|
85
|
+
'text-blue-500'
|
86
|
+
}`} />
|
87
|
+
) : alert.type === 'error' ? (
|
88
|
+
<XCircle className="w-5 h-5 text-red-500" />
|
89
|
+
) : (
|
90
|
+
<AlertCircle className="w-5 h-5 text-blue-500" />
|
91
|
+
)}
|
92
|
+
<p className="text-sm font-medium">{alert.message}</p>
|
93
|
+
</div>
|
94
|
+
<button
|
95
|
+
onClick={() => removeAlert(alert.id)}
|
96
|
+
className={`p-1 rounded-full hover:bg-opacity-10 ${
|
97
|
+
alert.type === 'success' ? 'hover:bg-green-900' :
|
98
|
+
alert.type === 'error' ? 'hover:bg-red-900' :
|
99
|
+
'hover:bg-blue-900'
|
100
|
+
}`}
|
101
|
+
>
|
102
|
+
<X className="w-4 h-4" />
|
103
|
+
</button>
|
104
|
+
</div>
|
105
|
+
))}
|
106
|
+
</div>
|
107
|
+
);
|
108
|
+
}
|
@@ -0,0 +1,161 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { Database, Table, ChevronDown, ChevronUp, BarChart, ChevronLeft, ChevronRight } from 'lucide-react';
|
3
|
+
import type { Dataset, Column } from '../types';
|
4
|
+
|
5
|
+
interface DatasetPreviewProps {
|
6
|
+
dataset: Dataset;
|
7
|
+
}
|
8
|
+
|
9
|
+
const STATS_PER_PAGE = 6;
|
10
|
+
|
11
|
+
export function DatasetPreview({ dataset }: DatasetPreviewProps) {
|
12
|
+
const [showStats, setShowStats] = useState(false);
|
13
|
+
const [currentPage, setCurrentPage] = useState(1);
|
14
|
+
const columns = dataset.sample_data[0] ? Object.keys(dataset.sample_data[0]) : [];
|
15
|
+
|
16
|
+
const totalPages = Math.ceil(dataset.columns.length / STATS_PER_PAGE);
|
17
|
+
const paginatedColumns = dataset.columns.slice(
|
18
|
+
(currentPage - 1) * STATS_PER_PAGE,
|
19
|
+
currentPage * STATS_PER_PAGE
|
20
|
+
);
|
21
|
+
|
22
|
+
return (
|
23
|
+
<div className="bg-white rounded-lg shadow-lg p-6">
|
24
|
+
<div className="flex items-start justify-between mb-6">
|
25
|
+
<div>
|
26
|
+
<div className="flex items-center gap-2">
|
27
|
+
<Database className="w-5 h-5 text-blue-600" />
|
28
|
+
<h3 className="text-xl font-semibold text-gray-900">{dataset.name}</h3>
|
29
|
+
</div>
|
30
|
+
<p className="text-gray-600 mt-1">{dataset.description}</p>
|
31
|
+
<p className="text-sm text-gray-500 mt-2">
|
32
|
+
{dataset.num_rows.toLocaleString()} rows • <span className="font-medium">Raw:</span> {dataset.columns.length} columns • <span className="font-medium">Processed:</span> {columns.length} columns • Last synced{' '}
|
33
|
+
{new Date(dataset.updated_at).toLocaleDateString()}
|
34
|
+
</p>
|
35
|
+
</div>
|
36
|
+
<button
|
37
|
+
onClick={() => setShowStats(!showStats)}
|
38
|
+
className="flex items-center gap-1 text-blue-600 hover:text-blue-800"
|
39
|
+
>
|
40
|
+
<BarChart className="w-4 h-4" />
|
41
|
+
<span className="text-sm font-medium">
|
42
|
+
{showStats ? 'Hide Statistics' : 'Show Statistics'}
|
43
|
+
</span>
|
44
|
+
{showStats ? (
|
45
|
+
<ChevronUp className="w-4 h-4" />
|
46
|
+
) : (
|
47
|
+
<ChevronDown className="w-4 h-4" />
|
48
|
+
)}
|
49
|
+
</button>
|
50
|
+
</div>
|
51
|
+
|
52
|
+
<div className="space-y-6">
|
53
|
+
{showStats && (
|
54
|
+
<>
|
55
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
56
|
+
{paginatedColumns.map((column: Column) => (
|
57
|
+
<div
|
58
|
+
key={column.name}
|
59
|
+
className="bg-gray-50 rounded-lg p-4"
|
60
|
+
>
|
61
|
+
<div className="flex items-center justify-between mb-2">
|
62
|
+
<h4 className="font-medium text-gray-900">{column.name}</h4>
|
63
|
+
<span className="text-xs font-medium text-gray-500 px-2 py-1 bg-gray-200 rounded-full">
|
64
|
+
{column.datatype}
|
65
|
+
</span>
|
66
|
+
</div>
|
67
|
+
<p className="text-sm text-gray-600 mb-3">{column.description}</p>
|
68
|
+
{column.statistics && (
|
69
|
+
<div className="space-y-1">
|
70
|
+
{Object.entries(column.statistics.raw).map(([key, value]) => {
|
71
|
+
if (key === "counts") {
|
72
|
+
return null;
|
73
|
+
}
|
74
|
+
return (
|
75
|
+
<div key={key} className="flex justify-between text-sm">
|
76
|
+
<span className="text-gray-500">
|
77
|
+
{key.charAt(0).toUpperCase() + key.slice(1)}:
|
78
|
+
</span>
|
79
|
+
<span className="font-medium text-gray-900">
|
80
|
+
{typeof value === 'number' ?
|
81
|
+
value.toLocaleString(undefined, {
|
82
|
+
maximumFractionDigits: 2
|
83
|
+
}) :
|
84
|
+
value}
|
85
|
+
</span>
|
86
|
+
</div>
|
87
|
+
)})}
|
88
|
+
</div>
|
89
|
+
)}
|
90
|
+
</div>
|
91
|
+
))}
|
92
|
+
</div>
|
93
|
+
|
94
|
+
{totalPages > 1 && (
|
95
|
+
<div className="flex items-center justify-between border-t pt-4">
|
96
|
+
<div className="text-sm text-gray-500">
|
97
|
+
Showing {((currentPage - 1) * STATS_PER_PAGE) + 1} to {Math.min(currentPage * STATS_PER_PAGE, dataset.columns.length)} of {dataset.columns.length} columns
|
98
|
+
</div>
|
99
|
+
<div className="flex items-center gap-2">
|
100
|
+
<button
|
101
|
+
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
102
|
+
disabled={currentPage === 1}
|
103
|
+
className="p-1 rounded-md hover:bg-gray-100 disabled:opacity-50"
|
104
|
+
>
|
105
|
+
<ChevronLeft className="w-5 h-5" />
|
106
|
+
</button>
|
107
|
+
<span className="text-sm text-gray-600">
|
108
|
+
Page {currentPage} of {totalPages}
|
109
|
+
</span>
|
110
|
+
<button
|
111
|
+
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
112
|
+
disabled={currentPage === totalPages}
|
113
|
+
className="p-1 rounded-md hover:bg-gray-100 disabled:opacity-50"
|
114
|
+
>
|
115
|
+
<ChevronRight className="w-5 h-5" />
|
116
|
+
</button>
|
117
|
+
</div>
|
118
|
+
</div>
|
119
|
+
)}
|
120
|
+
</>
|
121
|
+
)}
|
122
|
+
|
123
|
+
<div className="overflow-x-auto">
|
124
|
+
<div className="inline-block min-w-full align-middle">
|
125
|
+
<div className="overflow-hidden shadow-sm ring-1 ring-black ring-opacity-5 rounded-lg">
|
126
|
+
<table className="min-w-full divide-y divide-gray-300">
|
127
|
+
<thead className="bg-gray-50">
|
128
|
+
<tr>
|
129
|
+
{columns.map((column) => (
|
130
|
+
<th
|
131
|
+
key={column}
|
132
|
+
scope="col"
|
133
|
+
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
134
|
+
>
|
135
|
+
{column}
|
136
|
+
</th>
|
137
|
+
))}
|
138
|
+
</tr>
|
139
|
+
</thead>
|
140
|
+
<tbody className="divide-y divide-gray-200 bg-white">
|
141
|
+
{dataset.sample_data.map((row: Record<string, any>, i: number) => (
|
142
|
+
<tr key={i}>
|
143
|
+
{columns.map((column) => (
|
144
|
+
<td
|
145
|
+
key={row[column]}
|
146
|
+
className="whitespace-nowrap px-3 py-4 text-sm text-gray-500"
|
147
|
+
>
|
148
|
+
{row[column]?.toString()}
|
149
|
+
</td>
|
150
|
+
))}
|
151
|
+
</tr>
|
152
|
+
))}
|
153
|
+
</tbody>
|
154
|
+
</table>
|
155
|
+
</div>
|
156
|
+
</div>
|
157
|
+
</div>
|
158
|
+
</div>
|
159
|
+
</div>
|
160
|
+
);
|
161
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { LucideIcon } from 'lucide-react';
|
3
|
+
|
4
|
+
interface EmptyStateProps {
|
5
|
+
icon: LucideIcon;
|
6
|
+
title: string;
|
7
|
+
description: string;
|
8
|
+
actionLabel: string;
|
9
|
+
onAction: () => void;
|
10
|
+
}
|
11
|
+
|
12
|
+
export function EmptyState({ icon: Icon, title, description, actionLabel, onAction }: EmptyStateProps) {
|
13
|
+
return (
|
14
|
+
<div className="text-center py-12">
|
15
|
+
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
16
|
+
<Icon className="w-8 h-8 text-gray-400" />
|
17
|
+
</div>
|
18
|
+
<h3 className="text-lg font-medium text-gray-900 mb-2">{title}</h3>
|
19
|
+
<p className="text-gray-500 mb-6 max-w-sm mx-auto">{description}</p>
|
20
|
+
<button
|
21
|
+
onClick={onAction}
|
22
|
+
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"
|
23
|
+
>
|
24
|
+
{actionLabel}
|
25
|
+
</button>
|
26
|
+
</div>
|
27
|
+
);
|
28
|
+
}
|