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,235 @@
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
2
|
+
import { AlertCircle } from 'lucide-react';
|
3
|
+
import type { Dataset, FeatureGroup } from '../../types';
|
4
|
+
import { CodeEditor } from './CodeEditor';
|
5
|
+
import { DataPreview } from './DataPreview';
|
6
|
+
|
7
|
+
interface FeatureFormProps {
|
8
|
+
datasets: Dataset[];
|
9
|
+
groups: FeatureGroup[];
|
10
|
+
initialData?: {
|
11
|
+
name: string;
|
12
|
+
description: string;
|
13
|
+
groupId: number;
|
14
|
+
testDatasetId: number;
|
15
|
+
inputColumns: string[];
|
16
|
+
outputColumns: string[];
|
17
|
+
code: string;
|
18
|
+
};
|
19
|
+
onSubmit: (data: any) => void;
|
20
|
+
onCancel: () => void;
|
21
|
+
}
|
22
|
+
|
23
|
+
export function FeatureForm({
|
24
|
+
datasets,
|
25
|
+
groups,
|
26
|
+
initialData,
|
27
|
+
onSubmit,
|
28
|
+
onCancel
|
29
|
+
}: FeatureFormProps) {
|
30
|
+
const [formData, setFormData] = useState({
|
31
|
+
name: initialData?.name || '',
|
32
|
+
description: initialData?.description || '',
|
33
|
+
groupId: initialData?.groupId || '',
|
34
|
+
testDatasetId: initialData?.testDatasetId || '',
|
35
|
+
inputColumns: initialData?.inputColumns || [],
|
36
|
+
outputColumns: initialData?.outputColumns || [],
|
37
|
+
code: initialData?.code || ''
|
38
|
+
});
|
39
|
+
|
40
|
+
const [selectedDataset, setSelectedDataset] = useState<Dataset | null>(
|
41
|
+
initialData?.testDatasetId
|
42
|
+
? datasets.find(d => d.id === initialData.testDatasetId) || null
|
43
|
+
: null
|
44
|
+
);
|
45
|
+
|
46
|
+
const handleSubmit = (e: React.FormEvent) => {
|
47
|
+
e.preventDefault();
|
48
|
+
onSubmit(formData);
|
49
|
+
};
|
50
|
+
|
51
|
+
const handleDatasetChange = (datasetId: string) => {
|
52
|
+
const dataset = datasets.find(d => d.id === Number(datasetId)) || null;
|
53
|
+
setSelectedDataset(dataset);
|
54
|
+
setFormData(prev => ({
|
55
|
+
...prev,
|
56
|
+
testDatasetId: datasetId,
|
57
|
+
inputColumns: [],
|
58
|
+
outputColumns: []
|
59
|
+
}));
|
60
|
+
};
|
61
|
+
|
62
|
+
const toggleColumn = (columnName: string, type: 'input' | 'output') => {
|
63
|
+
setFormData(prev => ({
|
64
|
+
...prev,
|
65
|
+
[type === 'input' ? 'inputColumns' : 'outputColumns']:
|
66
|
+
prev[type === 'input' ? 'inputColumns' : 'outputColumns'].includes(columnName)
|
67
|
+
? prev[type === 'input' ? 'inputColumns' : 'outputColumns'].filter(c => c !== columnName)
|
68
|
+
: [...prev[type === 'input' ? 'inputColumns' : 'outputColumns'], columnName]
|
69
|
+
}));
|
70
|
+
};
|
71
|
+
|
72
|
+
return (
|
73
|
+
<form onSubmit={handleSubmit} className="p-6 space-y-8">
|
74
|
+
<div className="grid grid-cols-2 gap-6">
|
75
|
+
<div>
|
76
|
+
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
77
|
+
Name
|
78
|
+
</label>
|
79
|
+
<input
|
80
|
+
type="text"
|
81
|
+
id="name"
|
82
|
+
value={formData.name}
|
83
|
+
onChange={(e) => setFormData({ ...formData, name: 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"
|
85
|
+
required
|
86
|
+
/>
|
87
|
+
</div>
|
88
|
+
|
89
|
+
<div>
|
90
|
+
<label htmlFor="group" className="block text-sm font-medium text-gray-700">
|
91
|
+
Group
|
92
|
+
</label>
|
93
|
+
<select
|
94
|
+
id="group"
|
95
|
+
value={formData.groupId}
|
96
|
+
onChange={(e) => setFormData({ ...formData, groupId: e.target.value })}
|
97
|
+
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
98
|
+
required
|
99
|
+
>
|
100
|
+
<option value="">Select a group...</option>
|
101
|
+
{groups.map((group) => (
|
102
|
+
<option key={group.id} value={group.id}>
|
103
|
+
{group.name}
|
104
|
+
</option>
|
105
|
+
))}
|
106
|
+
</select>
|
107
|
+
</div>
|
108
|
+
</div>
|
109
|
+
|
110
|
+
<div>
|
111
|
+
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
|
112
|
+
Description
|
113
|
+
</label>
|
114
|
+
<textarea
|
115
|
+
id="description"
|
116
|
+
value={formData.description}
|
117
|
+
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
118
|
+
rows={3}
|
119
|
+
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
120
|
+
/>
|
121
|
+
</div>
|
122
|
+
|
123
|
+
<div>
|
124
|
+
<label htmlFor="dataset" className="block text-sm font-medium text-gray-700">
|
125
|
+
Test Dataset
|
126
|
+
</label>
|
127
|
+
<select
|
128
|
+
id="dataset"
|
129
|
+
value={formData.testDatasetId}
|
130
|
+
onChange={(e) => handleDatasetChange(e.target.value)}
|
131
|
+
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
132
|
+
required
|
133
|
+
>
|
134
|
+
<option value="">Select a dataset...</option>
|
135
|
+
{datasets.map((dataset) => (
|
136
|
+
<option key={dataset.id} value={dataset.id}>
|
137
|
+
{dataset.name}
|
138
|
+
</option>
|
139
|
+
))}
|
140
|
+
</select>
|
141
|
+
</div>
|
142
|
+
|
143
|
+
{selectedDataset && (
|
144
|
+
<div className="grid grid-cols-2 gap-6">
|
145
|
+
<div>
|
146
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
147
|
+
Input Columns
|
148
|
+
</label>
|
149
|
+
<div className="space-y-2">
|
150
|
+
{selectedDataset.columns.map((column) => (
|
151
|
+
<label
|
152
|
+
key={column.name}
|
153
|
+
className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50"
|
154
|
+
>
|
155
|
+
<input
|
156
|
+
type="checkbox"
|
157
|
+
checked={formData.inputColumns.includes(column.name)}
|
158
|
+
onChange={() => toggleColumn(column.name, 'input')}
|
159
|
+
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
160
|
+
/>
|
161
|
+
<span className="text-sm text-gray-900">{column.name}</span>
|
162
|
+
<span className="text-xs text-gray-500">({column.type})</span>
|
163
|
+
</label>
|
164
|
+
))}
|
165
|
+
</div>
|
166
|
+
</div>
|
167
|
+
|
168
|
+
<div>
|
169
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
170
|
+
Output Columns
|
171
|
+
</label>
|
172
|
+
<div className="space-y-2">
|
173
|
+
{selectedDataset.columns.map((column) => (
|
174
|
+
<label
|
175
|
+
key={column.name}
|
176
|
+
className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50"
|
177
|
+
>
|
178
|
+
<input
|
179
|
+
type="checkbox"
|
180
|
+
checked={formData.outputColumns.includes(column.name)}
|
181
|
+
onChange={() => toggleColumn(column.name, 'output')}
|
182
|
+
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
183
|
+
/>
|
184
|
+
<span className="text-sm text-gray-900">{column.name}</span>
|
185
|
+
<span className="text-xs text-gray-500">({column.type})</span>
|
186
|
+
</label>
|
187
|
+
))}
|
188
|
+
</div>
|
189
|
+
</div>
|
190
|
+
</div>
|
191
|
+
)}
|
192
|
+
|
193
|
+
<div>
|
194
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
195
|
+
Feature Code
|
196
|
+
</label>
|
197
|
+
<div className="bg-gray-50 rounded-lg p-4">
|
198
|
+
<CodeEditor
|
199
|
+
value={formData.code}
|
200
|
+
onChange={(code) => setFormData({ ...formData, code })}
|
201
|
+
language="ruby"
|
202
|
+
/>
|
203
|
+
</div>
|
204
|
+
</div>
|
205
|
+
|
206
|
+
{selectedDataset && formData.code && (
|
207
|
+
<div>
|
208
|
+
<h3 className="text-sm font-medium text-gray-900 mb-2">Preview</h3>
|
209
|
+
<DataPreview
|
210
|
+
dataset={selectedDataset}
|
211
|
+
code={formData.code}
|
212
|
+
inputColumns={formData.inputColumns}
|
213
|
+
outputColumns={formData.outputColumns}
|
214
|
+
/>
|
215
|
+
</div>
|
216
|
+
)}
|
217
|
+
|
218
|
+
<div className="flex justify-end gap-3 pt-6 border-t">
|
219
|
+
<button
|
220
|
+
type="button"
|
221
|
+
onClick={onCancel}
|
222
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900"
|
223
|
+
>
|
224
|
+
Cancel
|
225
|
+
</button>
|
226
|
+
<button
|
227
|
+
type="submit"
|
228
|
+
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"
|
229
|
+
>
|
230
|
+
{initialData ? 'Save Changes' : 'Create Feature'}
|
231
|
+
</button>
|
232
|
+
</div>
|
233
|
+
</form>
|
234
|
+
);
|
235
|
+
}
|
@@ -0,0 +1,54 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
// import { Link } from 'react-router-dom';
|
3
|
+
import { FolderOpen, Settings, Trash2 } from 'lucide-react';
|
4
|
+
import type { FeatureGroup } from '../../types';
|
5
|
+
|
6
|
+
interface FeatureGroupCardProps {
|
7
|
+
group: FeatureGroup;
|
8
|
+
}
|
9
|
+
|
10
|
+
export function FeatureGroupCard({ group }: FeatureGroupCardProps) {
|
11
|
+
return (
|
12
|
+
<div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
|
13
|
+
<div className="flex justify-between items-start mb-4">
|
14
|
+
<div className="flex items-start gap-3">
|
15
|
+
<FolderOpen className="w-5 h-5 text-blue-600 mt-1" />
|
16
|
+
<div>
|
17
|
+
<h3 className="text-lg font-semibold text-gray-900">
|
18
|
+
{group.name}
|
19
|
+
</h3>
|
20
|
+
<p className="text-sm text-gray-500 mt-1">
|
21
|
+
{group.description}
|
22
|
+
</p>
|
23
|
+
</div>
|
24
|
+
</div>
|
25
|
+
<div className="flex gap-2">
|
26
|
+
<Link
|
27
|
+
to={`/features/groups/${group.id}/edit`}
|
28
|
+
className="text-gray-400 hover:text-blue-600 transition-colors"
|
29
|
+
title="Edit group"
|
30
|
+
>
|
31
|
+
<Settings className="w-5 h-5" />
|
32
|
+
</Link>
|
33
|
+
<button
|
34
|
+
className="text-gray-400 hover:text-red-600 transition-colors"
|
35
|
+
title="Delete group"
|
36
|
+
>
|
37
|
+
<Trash2 className="w-5 h-5" />
|
38
|
+
</button>
|
39
|
+
</div>
|
40
|
+
</div>
|
41
|
+
|
42
|
+
<div className="mt-4 pt-4 border-t border-gray-100">
|
43
|
+
<div className="flex items-center justify-between text-sm">
|
44
|
+
<span className="text-gray-500">
|
45
|
+
{group.features.length} features
|
46
|
+
</span>
|
47
|
+
<span className="text-gray-500">
|
48
|
+
Last updated {new Date(group.updatedAt).toLocaleDateString()}
|
49
|
+
</span>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
</div>
|
53
|
+
);
|
54
|
+
}
|
@@ -0,0 +1,81 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { Puzzle, Key, ExternalLink } from 'lucide-react';
|
3
|
+
|
4
|
+
interface PluginSettingsProps {
|
5
|
+
settings: {
|
6
|
+
wandb_api_key: string;
|
7
|
+
};
|
8
|
+
setData: (data: any) => void;
|
9
|
+
}
|
10
|
+
|
11
|
+
export function PluginSettings({ settings, setData }: PluginSettingsProps) {
|
12
|
+
const [showApiKey, setShowApiKey] = useState(false);
|
13
|
+
|
14
|
+
return (
|
15
|
+
<div className="space-y-4">
|
16
|
+
<div className="flex items-center gap-2 mb-4">
|
17
|
+
<Puzzle className="w-5 h-5 text-gray-500" />
|
18
|
+
<h3 className="text-lg font-medium text-gray-900">Plugins</h3>
|
19
|
+
</div>
|
20
|
+
|
21
|
+
<div className="space-y-6">
|
22
|
+
<div className="border border-gray-200 rounded-lg p-4">
|
23
|
+
<div className="flex items-start justify-between">
|
24
|
+
<div className="flex items-start gap-3">
|
25
|
+
<img
|
26
|
+
src="https://raw.githubusercontent.com/wandb/assets/main/wandb-dots-logo.svg"
|
27
|
+
alt="Weights & Biases"
|
28
|
+
className="w-8 h-8"
|
29
|
+
/>
|
30
|
+
<div>
|
31
|
+
<h4 className="text-base font-medium text-gray-900">Weights & Biases</h4>
|
32
|
+
<p className="text-sm text-gray-500 mt-1">
|
33
|
+
Track and visualize machine learning experiments
|
34
|
+
</p>
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
<a
|
38
|
+
href="https://wandb.ai/settings"
|
39
|
+
target="_blank"
|
40
|
+
rel="noopener noreferrer"
|
41
|
+
className="text-blue-600 hover:text-blue-700 inline-flex items-center gap-1 text-sm"
|
42
|
+
>
|
43
|
+
Get API Key
|
44
|
+
<ExternalLink className="w-4 h-4" />
|
45
|
+
</a>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<div className="mt-4">
|
49
|
+
<label htmlFor="wandb_api_key" className="block text-sm font-medium text-gray-700">
|
50
|
+
API Key
|
51
|
+
</label>
|
52
|
+
<div className="mt-1 relative rounded-md shadow-sm">
|
53
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
54
|
+
<Key className="h-5 w-5 text-gray-400" />
|
55
|
+
</div>
|
56
|
+
<input
|
57
|
+
type={showApiKey ? "text" : "password"}
|
58
|
+
name="wandb_api_key"
|
59
|
+
id="wandb_api_key"
|
60
|
+
value={settings.wandb_api_key}
|
61
|
+
onChange={(e) => setData({ settings: { ...settings, wandb_api_key: e.target.value } })}
|
62
|
+
className="focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md"
|
63
|
+
placeholder="Enter your Weights & Biases API key"
|
64
|
+
/>
|
65
|
+
<button
|
66
|
+
type="button"
|
67
|
+
onClick={() => setShowApiKey(!showApiKey)}
|
68
|
+
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
69
|
+
>
|
70
|
+
<Key className={`h-5 w-5 ${showApiKey ? 'text-gray-400' : 'text-gray-600'}`} />
|
71
|
+
</button>
|
72
|
+
</div>
|
73
|
+
<p className="mt-1 text-xs text-gray-500">
|
74
|
+
Your API key will be used to log metrics, artifacts, and experiment results
|
75
|
+
</p>
|
76
|
+
</div>
|
77
|
+
</div>
|
78
|
+
</div>
|
79
|
+
</div>
|
80
|
+
);
|
81
|
+
}
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import * as React from "react"
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
3
|
+
|
4
|
+
import { cn } from "@/lib/utils"
|
5
|
+
|
6
|
+
const badgeVariants = cva(
|
7
|
+
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
8
|
+
{
|
9
|
+
variants: {
|
10
|
+
variant: {
|
11
|
+
default:
|
12
|
+
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
13
|
+
secondary:
|
14
|
+
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
15
|
+
destructive:
|
16
|
+
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
17
|
+
outline: "text-foreground",
|
18
|
+
information:
|
19
|
+
"border-transparent bg-blue-100 text-blue-800 hover:bg-blue-600",
|
20
|
+
important:
|
21
|
+
"border-transparent bg-red-100 text-red-800 hover:bg-red-600",
|
22
|
+
warning:
|
23
|
+
"border-transparent bg-yellow-100 text-yellow-800 hover:bg-yellow-600",
|
24
|
+
success:
|
25
|
+
"border-transparent bg-green-100 text-green-800 hover:bg-green-600",
|
26
|
+
},
|
27
|
+
},
|
28
|
+
defaultVariants: {
|
29
|
+
variant: "default",
|
30
|
+
},
|
31
|
+
}
|
32
|
+
)
|
33
|
+
|
34
|
+
export interface BadgeProps
|
35
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
36
|
+
VariantProps<typeof badgeVariants> {}
|
37
|
+
|
38
|
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
39
|
+
return (
|
40
|
+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
41
|
+
)
|
42
|
+
}
|
43
|
+
|
44
|
+
export { Badge, badgeVariants }
|
@@ -0,0 +1,9 @@
|
|
1
|
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
2
|
+
|
3
|
+
const Collapsible = CollapsiblePrimitive.Root
|
4
|
+
|
5
|
+
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
6
|
+
|
7
|
+
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
8
|
+
|
9
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import * as React from "react"
|
2
|
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
3
|
+
|
4
|
+
import { cn } from "@/lib/utils"
|
5
|
+
|
6
|
+
const ScrollArea = React.forwardRef<
|
7
|
+
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
8
|
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
9
|
+
>(({ className, children, ...props }, ref) => (
|
10
|
+
<ScrollAreaPrimitive.Root
|
11
|
+
ref={ref}
|
12
|
+
className={cn("relative overflow-hidden", className)}
|
13
|
+
{...props}
|
14
|
+
>
|
15
|
+
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
16
|
+
{children}
|
17
|
+
</ScrollAreaPrimitive.Viewport>
|
18
|
+
<ScrollBar />
|
19
|
+
<ScrollAreaPrimitive.Corner />
|
20
|
+
</ScrollAreaPrimitive.Root>
|
21
|
+
))
|
22
|
+
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
23
|
+
|
24
|
+
const ScrollBar = React.forwardRef<
|
25
|
+
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
26
|
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
27
|
+
>(({ className, orientation = "vertical", ...props }, ref) => (
|
28
|
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
29
|
+
ref={ref}
|
30
|
+
orientation={orientation}
|
31
|
+
className={cn(
|
32
|
+
"flex touch-none select-none transition-colors",
|
33
|
+
orientation === "vertical" &&
|
34
|
+
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
35
|
+
orientation === "horizontal" &&
|
36
|
+
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
37
|
+
className
|
38
|
+
)}
|
39
|
+
{...props}
|
40
|
+
>
|
41
|
+
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
42
|
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
43
|
+
))
|
44
|
+
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
45
|
+
|
46
|
+
export { ScrollArea, ScrollBar }
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import * as React from "react"
|
2
|
+
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
3
|
+
|
4
|
+
import { cn } from "@/lib/utils"
|
5
|
+
|
6
|
+
const Separator = React.forwardRef<
|
7
|
+
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
8
|
+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
9
|
+
>(
|
10
|
+
(
|
11
|
+
{ className, orientation = "horizontal", decorative = true, ...props },
|
12
|
+
ref
|
13
|
+
) => (
|
14
|
+
<SeparatorPrimitive.Root
|
15
|
+
ref={ref}
|
16
|
+
decorative={decorative}
|
17
|
+
orientation={orientation}
|
18
|
+
className={cn(
|
19
|
+
"shrink-0 bg-border",
|
20
|
+
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
21
|
+
className
|
22
|
+
)}
|
23
|
+
{...props}
|
24
|
+
/>
|
25
|
+
)
|
26
|
+
)
|
27
|
+
Separator.displayName = SeparatorPrimitive.Root.displayName
|
28
|
+
|
29
|
+
export { Separator }
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
3
|
+
import { Navigation } from './components/Navigation';
|
4
|
+
import { ModelsPage } from './pages/ModelsPage';
|
5
|
+
import { NewModelPage } from './pages/NewModelPage';
|
6
|
+
import { EditModelPage } from './pages/EditModelPage';
|
7
|
+
import { DatasourcesPage } from './pages/DatasourcesPage';
|
8
|
+
import { NewDatasourcePage } from './pages/NewDatasourcePage';
|
9
|
+
import { EditDatasourcePage } from './pages/EditDatasourcePage';
|
10
|
+
import { DatasetsPage } from './pages/DatasetsPage';
|
11
|
+
import { NewDatasetPage } from './pages/NewDatasetPage';
|
12
|
+
import { DatasetDetailsPage } from './pages/DatasetDetailsPage';
|
13
|
+
import { SettingsPage } from './pages/SettingsPage';
|
14
|
+
import { FeaturesPage } from './pages/FeaturesPage';
|
15
|
+
import { NewFeaturePage } from './pages/NewFeaturePage';
|
16
|
+
import { EditFeaturePage } from './pages/EditFeaturePage';
|
17
|
+
|
18
|
+
export default function App() {
|
19
|
+
return (
|
20
|
+
<BrowserRouter>
|
21
|
+
<Navigation>
|
22
|
+
<Routes>
|
23
|
+
<Route path="/" element={<ModelsPage />} />
|
24
|
+
<Route path="/models/new" element={<NewModelPage />} />
|
25
|
+
<Route path="/models/:id/edit" element={<EditModelPage />} />
|
26
|
+
<Route path="/datasources" element={<DatasourcesPage />} />
|
27
|
+
<Route path="/datasources/new" element={<NewDatasourcePage />} />
|
28
|
+
<Route path="/datasources/:id/edit" element={<EditDatasourcePage />} />
|
29
|
+
<Route path="/datasets" element={<DatasetsPage />} />
|
30
|
+
<Route path="/datasets/new" element={<NewDatasetPage />} />
|
31
|
+
<Route path="/datasets/:id" element={<DatasetDetailsPage />} />
|
32
|
+
<Route path="/features" element={<FeaturesPage />} />
|
33
|
+
<Route path="/features/new" element={<NewFeaturePage />} />
|
34
|
+
<Route path="/features/:id/edit" element={<EditFeaturePage />} />
|
35
|
+
<Route path="/settings" element={<SettingsPage />} />
|
36
|
+
</Routes>
|
37
|
+
</Navigation>
|
38
|
+
</BrowserRouter>
|
39
|
+
);
|
40
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import "../styles/application.css"
|
2
|
+
import { createInertiaApp } from '@inertiajs/react'
|
3
|
+
import { createRoot } from 'react-dom/client'
|
4
|
+
import Layout from '../layouts/Layout';
|
5
|
+
|
6
|
+
document.addEventListener('DOMContentLoaded', () => {
|
7
|
+
createInertiaApp({
|
8
|
+
resolve: name => {
|
9
|
+
const pages = import.meta.glob('../pages/**/*.tsx', { eager: true })
|
10
|
+
let page = pages[`../${name}.tsx`];
|
11
|
+
if (!page.default) {
|
12
|
+
alert(`The page ${name} could not be found, you probably forgot to export default.`);
|
13
|
+
return;
|
14
|
+
}
|
15
|
+
page.default.layout = page.default.layout || (page => <Layout children={page} />)
|
16
|
+
return page;
|
17
|
+
},
|
18
|
+
setup({ el, App, props }) {
|
19
|
+
createRoot(el).render(
|
20
|
+
<App {...props} />
|
21
|
+
)
|
22
|
+
},
|
23
|
+
})
|
24
|
+
})
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
2
|
+
import debounce from 'lodash/debounce';
|
3
|
+
import isEqual from 'lodash/isEqual';
|
4
|
+
|
5
|
+
interface AutosaveStatus {
|
6
|
+
saving: boolean;
|
7
|
+
saved: boolean;
|
8
|
+
error: string | null;
|
9
|
+
}
|
10
|
+
|
11
|
+
export function useAutosave<T>(
|
12
|
+
data: T,
|
13
|
+
onSave: (data: T) => Promise<void>,
|
14
|
+
debounceMs: number = 1000
|
15
|
+
) {
|
16
|
+
const [status, setStatus] = useState<AutosaveStatus>({
|
17
|
+
saving: false,
|
18
|
+
saved: false,
|
19
|
+
error: null
|
20
|
+
});
|
21
|
+
|
22
|
+
const previousSerializedData = useRef(JSON.stringify(data));
|
23
|
+
|
24
|
+
const debouncedSave = useCallback(
|
25
|
+
debounce(async (newData: T) => {
|
26
|
+
setStatus(prev => ({ ...prev, saving: true, error: null }));
|
27
|
+
try {
|
28
|
+
await onSave(newData);
|
29
|
+
previousSerializedData.current = JSON.stringify(newData); // Update reference after saving
|
30
|
+
setStatus({ saving: false, saved: true, error: null });
|
31
|
+
|
32
|
+
// Reset "saved" status after 3 seconds
|
33
|
+
setTimeout(() => {
|
34
|
+
setStatus(prev => ({ ...prev, saved: false }));
|
35
|
+
}, 4000);
|
36
|
+
} catch (err) {
|
37
|
+
setStatus({
|
38
|
+
saving: false,
|
39
|
+
saved: false,
|
40
|
+
error: err instanceof Error ? err.message : 'Failed to save changes'
|
41
|
+
});
|
42
|
+
}
|
43
|
+
}, debounceMs),
|
44
|
+
[onSave, debounceMs]
|
45
|
+
);
|
46
|
+
|
47
|
+
useEffect(() => {
|
48
|
+
// Serialize current data for deep comparison
|
49
|
+
const serializedData = JSON.stringify(data);
|
50
|
+
|
51
|
+
if (serializedData !== previousSerializedData.current) {
|
52
|
+
debouncedSave(data); // Trigger save if there's a difference
|
53
|
+
}
|
54
|
+
|
55
|
+
return () => {
|
56
|
+
debouncedSave.cancel();
|
57
|
+
};
|
58
|
+
}, [data, debouncedSave]);
|
59
|
+
|
60
|
+
return status;
|
61
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import React, { useEffect } from "react";
|
2
|
+
import { Navigation } from "../components/Navigation";
|
3
|
+
import { AlertProvider, useAlerts } from '../components/AlertProvider';
|
4
|
+
import { usePage } from '@inertiajs/react';
|
5
|
+
|
6
|
+
interface PageProps {
|
7
|
+
flash: Array<{
|
8
|
+
type: 'success' | 'error' | 'info';
|
9
|
+
message: string;
|
10
|
+
}>;
|
11
|
+
}
|
12
|
+
|
13
|
+
function FlashMessageHandler({ children }: { children: React.ReactNode }) {
|
14
|
+
const { showAlert } = useAlerts();
|
15
|
+
const { flash } = usePage<PageProps>().props;
|
16
|
+
|
17
|
+
useEffect(() => {
|
18
|
+
if (flash) {
|
19
|
+
flash.forEach(({ type, message }) => {
|
20
|
+
showAlert(type, message);
|
21
|
+
});
|
22
|
+
}
|
23
|
+
}, [flash, showAlert]);
|
24
|
+
|
25
|
+
return <>{children}</>;
|
26
|
+
}
|
27
|
+
|
28
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
29
|
+
return (
|
30
|
+
<AlertProvider>
|
31
|
+
<FlashMessageHandler>
|
32
|
+
<Navigation>
|
33
|
+
{children}
|
34
|
+
</Navigation>
|
35
|
+
</FlashMessageHandler>
|
36
|
+
</AlertProvider>
|
37
|
+
);
|
38
|
+
}
|