flowyml 1.1.0__py3-none-any.whl
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.
- flowyml/__init__.py +207 -0
- flowyml/assets/__init__.py +22 -0
- flowyml/assets/artifact.py +40 -0
- flowyml/assets/base.py +209 -0
- flowyml/assets/dataset.py +100 -0
- flowyml/assets/featureset.py +301 -0
- flowyml/assets/metrics.py +104 -0
- flowyml/assets/model.py +82 -0
- flowyml/assets/registry.py +157 -0
- flowyml/assets/report.py +315 -0
- flowyml/cli/__init__.py +5 -0
- flowyml/cli/experiment.py +232 -0
- flowyml/cli/init.py +256 -0
- flowyml/cli/main.py +327 -0
- flowyml/cli/run.py +75 -0
- flowyml/cli/stack_cli.py +532 -0
- flowyml/cli/ui.py +33 -0
- flowyml/core/__init__.py +68 -0
- flowyml/core/advanced_cache.py +274 -0
- flowyml/core/approval.py +64 -0
- flowyml/core/cache.py +203 -0
- flowyml/core/checkpoint.py +148 -0
- flowyml/core/conditional.py +373 -0
- flowyml/core/context.py +155 -0
- flowyml/core/error_handling.py +419 -0
- flowyml/core/executor.py +354 -0
- flowyml/core/graph.py +185 -0
- flowyml/core/parallel.py +452 -0
- flowyml/core/pipeline.py +764 -0
- flowyml/core/project.py +253 -0
- flowyml/core/resources.py +424 -0
- flowyml/core/scheduler.py +630 -0
- flowyml/core/scheduler_config.py +32 -0
- flowyml/core/step.py +201 -0
- flowyml/core/step_grouping.py +292 -0
- flowyml/core/templates.py +226 -0
- flowyml/core/versioning.py +217 -0
- flowyml/integrations/__init__.py +1 -0
- flowyml/integrations/keras.py +134 -0
- flowyml/monitoring/__init__.py +1 -0
- flowyml/monitoring/alerts.py +57 -0
- flowyml/monitoring/data.py +102 -0
- flowyml/monitoring/llm.py +160 -0
- flowyml/monitoring/monitor.py +57 -0
- flowyml/monitoring/notifications.py +246 -0
- flowyml/registry/__init__.py +5 -0
- flowyml/registry/model_registry.py +491 -0
- flowyml/registry/pipeline_registry.py +55 -0
- flowyml/stacks/__init__.py +27 -0
- flowyml/stacks/base.py +77 -0
- flowyml/stacks/bridge.py +288 -0
- flowyml/stacks/components.py +155 -0
- flowyml/stacks/gcp.py +499 -0
- flowyml/stacks/local.py +112 -0
- flowyml/stacks/migration.py +97 -0
- flowyml/stacks/plugin_config.py +78 -0
- flowyml/stacks/plugins.py +401 -0
- flowyml/stacks/registry.py +226 -0
- flowyml/storage/__init__.py +26 -0
- flowyml/storage/artifacts.py +246 -0
- flowyml/storage/materializers/__init__.py +20 -0
- flowyml/storage/materializers/base.py +133 -0
- flowyml/storage/materializers/keras.py +185 -0
- flowyml/storage/materializers/numpy.py +94 -0
- flowyml/storage/materializers/pandas.py +142 -0
- flowyml/storage/materializers/pytorch.py +135 -0
- flowyml/storage/materializers/sklearn.py +110 -0
- flowyml/storage/materializers/tensorflow.py +152 -0
- flowyml/storage/metadata.py +931 -0
- flowyml/tracking/__init__.py +1 -0
- flowyml/tracking/experiment.py +211 -0
- flowyml/tracking/leaderboard.py +191 -0
- flowyml/tracking/runs.py +145 -0
- flowyml/ui/__init__.py +15 -0
- flowyml/ui/backend/Dockerfile +31 -0
- flowyml/ui/backend/__init__.py +0 -0
- flowyml/ui/backend/auth.py +163 -0
- flowyml/ui/backend/main.py +187 -0
- flowyml/ui/backend/routers/__init__.py +0 -0
- flowyml/ui/backend/routers/assets.py +45 -0
- flowyml/ui/backend/routers/execution.py +179 -0
- flowyml/ui/backend/routers/experiments.py +49 -0
- flowyml/ui/backend/routers/leaderboard.py +118 -0
- flowyml/ui/backend/routers/notifications.py +72 -0
- flowyml/ui/backend/routers/pipelines.py +110 -0
- flowyml/ui/backend/routers/plugins.py +192 -0
- flowyml/ui/backend/routers/projects.py +85 -0
- flowyml/ui/backend/routers/runs.py +66 -0
- flowyml/ui/backend/routers/schedules.py +222 -0
- flowyml/ui/backend/routers/traces.py +84 -0
- flowyml/ui/frontend/Dockerfile +20 -0
- flowyml/ui/frontend/README.md +315 -0
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
- flowyml/ui/frontend/dist/index.html +16 -0
- flowyml/ui/frontend/index.html +15 -0
- flowyml/ui/frontend/nginx.conf +26 -0
- flowyml/ui/frontend/package-lock.json +3545 -0
- flowyml/ui/frontend/package.json +33 -0
- flowyml/ui/frontend/postcss.config.js +6 -0
- flowyml/ui/frontend/src/App.jsx +21 -0
- flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
- flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
- flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
- flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
- flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
- flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
- flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
- flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
- flowyml/ui/frontend/src/components/Layout.jsx +108 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
- flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
- flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
- flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
- flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
- flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
- flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
- flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
- flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
- flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
- flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
- flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
- flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
- flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
- flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
- flowyml/ui/frontend/src/index.css +11 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
- flowyml/ui/frontend/src/main.jsx +10 -0
- flowyml/ui/frontend/src/router/index.jsx +39 -0
- flowyml/ui/frontend/src/services/pluginService.js +90 -0
- flowyml/ui/frontend/src/utils/api.js +47 -0
- flowyml/ui/frontend/src/utils/cn.js +6 -0
- flowyml/ui/frontend/tailwind.config.js +31 -0
- flowyml/ui/frontend/vite.config.js +21 -0
- flowyml/ui/utils.py +77 -0
- flowyml/utils/__init__.py +67 -0
- flowyml/utils/config.py +308 -0
- flowyml/utils/debug.py +240 -0
- flowyml/utils/environment.py +346 -0
- flowyml/utils/git.py +319 -0
- flowyml/utils/logging.py +61 -0
- flowyml/utils/performance.py +314 -0
- flowyml/utils/stack_config.py +296 -0
- flowyml/utils/validation.py +270 -0
- flowyml-1.1.0.dist-info/METADATA +372 -0
- flowyml-1.1.0.dist-info/RECORD +159 -0
- flowyml-1.1.0.dist-info/WHEEL +4 -0
- flowyml-1.1.0.dist-info/entry_points.txt +3 -0
- flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { fetchApi } from '../../utils/api';
|
|
3
|
+
import { Link } from 'react-router-dom';
|
|
4
|
+
import { Folder, Plus, Trash2, Activity, Database, Clock } from 'lucide-react';
|
|
5
|
+
import { format } from 'date-fns';
|
|
6
|
+
import { DataView } from '../../components/ui/DataView';
|
|
7
|
+
import { Button } from '../../components/ui/Button';
|
|
8
|
+
import { useProject } from '../../contexts/ProjectContext';
|
|
9
|
+
|
|
10
|
+
export function Projects() {
|
|
11
|
+
const [projects, setProjects] = useState([]);
|
|
12
|
+
const [loading, setLoading] = useState(true);
|
|
13
|
+
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
14
|
+
const [newProjectName, setNewProjectName] = useState('');
|
|
15
|
+
const [newProjectDesc, setNewProjectDesc] = useState('');
|
|
16
|
+
const [projectStats, setProjectStats] = useState({});
|
|
17
|
+
const { setSelectedProject } = useProject();
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
fetchProjects();
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
const fetchProjects = async () => {
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetchApi('/api/projects/');
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
// setProjects(data); // This line will be replaced/modified
|
|
28
|
+
|
|
29
|
+
// Fetch stats for each project
|
|
30
|
+
const projectsList = Array.isArray(data) ? data : (data.projects || []);
|
|
31
|
+
const projectsWithStats = await Promise.all(projectsList.map(async (project) => {
|
|
32
|
+
try {
|
|
33
|
+
// Fetch runs for this project
|
|
34
|
+
const runsRes = await fetch(`/api/runs?project=${encodeURIComponent(project.name)}`);
|
|
35
|
+
const runsData = await runsRes.json();
|
|
36
|
+
|
|
37
|
+
// Fetch pipelines - we need to count unique pipelines from runs
|
|
38
|
+
const pipelineNames = new Set((runsData.runs || []).map(r => r.pipeline_name));
|
|
39
|
+
|
|
40
|
+
// Fetch artifacts for this project
|
|
41
|
+
const artifactsRes = await fetch(`/api/assets?project=${encodeURIComponent(project.name)}`);
|
|
42
|
+
const artifactsData = await artifactsRes.json();
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...project,
|
|
46
|
+
runs: (runsData.runs || []).length,
|
|
47
|
+
pipelines: pipelineNames.size,
|
|
48
|
+
artifacts: (artifactsData.artifacts || []).length
|
|
49
|
+
};
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(`Failed to fetch stats for project ${project.name}:`, err);
|
|
52
|
+
return {
|
|
53
|
+
...project,
|
|
54
|
+
runs: 0,
|
|
55
|
+
pipelines: 0,
|
|
56
|
+
artifacts: 0
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
setProjects(projectsWithStats);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Failed to fetch projects:', error);
|
|
64
|
+
} finally {
|
|
65
|
+
setLoading(false);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const createProject = async (e) => {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetchApi('/api/projects/', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
name: newProjectName,
|
|
77
|
+
description: newProjectDesc
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (response.ok) {
|
|
82
|
+
setShowCreateModal(false);
|
|
83
|
+
setNewProjectName('');
|
|
84
|
+
setNewProjectDesc('');
|
|
85
|
+
fetchProjects();
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Failed to create project:', error);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const deleteProject = async (name) => {
|
|
93
|
+
if (!confirm(`Are you sure you want to delete project "${name}"?`)) return;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const response = await fetchApi(`/api/projects/${name}`, {
|
|
97
|
+
method: 'DELETE'
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (response.ok) {
|
|
101
|
+
fetchProjects();
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Failed to delete project:', error);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const columns = [
|
|
109
|
+
{
|
|
110
|
+
header: 'Project Name',
|
|
111
|
+
key: 'name',
|
|
112
|
+
sortable: true,
|
|
113
|
+
render: (project) => (
|
|
114
|
+
<div className="flex items-center gap-3">
|
|
115
|
+
<div className="p-2 bg-blue-500/10 rounded-lg">
|
|
116
|
+
<Folder className="w-5 h-5 text-blue-500" />
|
|
117
|
+
</div>
|
|
118
|
+
<div>
|
|
119
|
+
<div className="font-medium text-slate-900 dark:text-white">{project.name}</div>
|
|
120
|
+
<div className="text-xs text-slate-500">Created {format(new Date(project.created_at), 'MMM d, yyyy')}</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
header: 'Description',
|
|
127
|
+
key: 'description',
|
|
128
|
+
render: (project) => (
|
|
129
|
+
<span className="text-slate-500 dark:text-slate-400 truncate max-w-xs block">
|
|
130
|
+
{project.description || "No description"}
|
|
131
|
+
</span>
|
|
132
|
+
)
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
header: 'Stats',
|
|
136
|
+
key: 'stats',
|
|
137
|
+
render: (project) => {
|
|
138
|
+
const stats = projectStats[project.name] || { runs: 0, pipelines: 0, artifacts: 0 };
|
|
139
|
+
return (
|
|
140
|
+
<div className="flex gap-4 text-sm text-slate-500">
|
|
141
|
+
<span className="flex items-center gap-1"><Activity size={14} /> {stats.pipelines || 0}</span>
|
|
142
|
+
<span className="flex items-center gap-1"><Clock size={14} /> {stats.runs || 0}</span>
|
|
143
|
+
<span className="flex items-center gap-1"><Database size={14} /> {stats.artifacts || 0}</span>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
header: 'Actions',
|
|
150
|
+
key: 'actions',
|
|
151
|
+
render: (project) => (
|
|
152
|
+
<button
|
|
153
|
+
onClick={(e) => { e.stopPropagation(); deleteProject(project.name); }}
|
|
154
|
+
className="p-2 text-slate-400 hover:text-red-500 transition-colors"
|
|
155
|
+
>
|
|
156
|
+
<Trash2 size={16} />
|
|
157
|
+
</button>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
const renderGrid = (project) => {
|
|
163
|
+
const stats = projectStats[project.name] || { runs: 0, pipelines: 0, artifacts: 0 };
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<Link
|
|
167
|
+
to={`/runs?project=${encodeURIComponent(project.name)}`}
|
|
168
|
+
onClick={() => setSelectedProject(project.name)}
|
|
169
|
+
className="block"
|
|
170
|
+
>
|
|
171
|
+
<div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-6 hover:border-blue-500/50 hover:shadow-md transition-all group cursor-pointer">
|
|
172
|
+
<div className="flex justify-between items-start mb-4">
|
|
173
|
+
<div className="flex items-center gap-3">
|
|
174
|
+
<div className="p-3 bg-blue-500/10 rounded-xl group-hover:bg-blue-500/20 transition-colors">
|
|
175
|
+
<Folder className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
|
176
|
+
</div>
|
|
177
|
+
<div>
|
|
178
|
+
<h3 className="font-bold text-lg text-slate-900 dark:text-white">{project.name}</h3>
|
|
179
|
+
<p className="text-xs text-slate-500">
|
|
180
|
+
Created {format(new Date(project.created_at), 'MMM d, yyyy')}
|
|
181
|
+
</p>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
<button
|
|
185
|
+
onClick={(e) => { e.preventDefault(); e.stopPropagation(); deleteProject(project.name); }}
|
|
186
|
+
className="text-slate-400 hover:text-red-500 transition-colors opacity-0 group-hover:opacity-100"
|
|
187
|
+
>
|
|
188
|
+
<Trash2 className="w-4 h-4" />
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<p className="text-slate-500 dark:text-slate-400 mb-6 h-10 overflow-hidden text-sm line-clamp-2">
|
|
193
|
+
{project.description || "No description provided."}
|
|
194
|
+
</p>
|
|
195
|
+
|
|
196
|
+
<div className="grid grid-cols-3 gap-4 border-t border-slate-100 dark:border-slate-700 pt-4">
|
|
197
|
+
<div className="text-center">
|
|
198
|
+
<div className="flex items-center justify-center gap-1 text-slate-400 text-xs mb-1">
|
|
199
|
+
<Activity className="w-3 h-3" /> Pipelines
|
|
200
|
+
</div>
|
|
201
|
+
<span className="font-bold text-slate-700 dark:text-slate-200">{stats.pipelines || 0}</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div className="text-center">
|
|
204
|
+
<div className="flex items-center justify-center gap-1 text-slate-400 text-xs mb-1">
|
|
205
|
+
<Clock className="w-3 h-3" /> Runs
|
|
206
|
+
</div>
|
|
207
|
+
<span className="font-bold text-slate-700 dark:text-slate-200">{stats.runs || 0}</span>
|
|
208
|
+
</div>
|
|
209
|
+
<div className="text-center">
|
|
210
|
+
<div className="flex items-center justify-center gap-1 text-slate-400 text-xs mb-1">
|
|
211
|
+
<Database className="w-3 h-3" /> Artifacts
|
|
212
|
+
</div>
|
|
213
|
+
<span className="font-bold text-slate-700 dark:text-slate-200">{stats.artifacts || 0}</span>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</Link>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div className="p-6 max-w-7xl mx-auto">
|
|
223
|
+
<DataView
|
|
224
|
+
title="Projects"
|
|
225
|
+
subtitle="Manage your ML projects and workspaces"
|
|
226
|
+
items={projects}
|
|
227
|
+
loading={loading}
|
|
228
|
+
columns={columns}
|
|
229
|
+
renderGrid={renderGrid}
|
|
230
|
+
actions={
|
|
231
|
+
<Button onClick={() => setShowCreateModal(true)} className="flex items-center gap-2">
|
|
232
|
+
<Plus size={18} />
|
|
233
|
+
New Project
|
|
234
|
+
</Button>
|
|
235
|
+
}
|
|
236
|
+
emptyState={
|
|
237
|
+
<div className="text-center py-12 bg-slate-50 dark:bg-slate-800/30 rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-700">
|
|
238
|
+
<Folder className="w-12 h-12 mx-auto text-slate-400 mb-4" />
|
|
239
|
+
<h3 className="text-lg font-medium text-slate-900 dark:text-white">No projects found</h3>
|
|
240
|
+
<p className="text-slate-500 mb-6">Get started by creating your first project.</p>
|
|
241
|
+
<Button onClick={() => setShowCreateModal(true)}>
|
|
242
|
+
Create Project
|
|
243
|
+
</Button>
|
|
244
|
+
</div>
|
|
245
|
+
}
|
|
246
|
+
/>
|
|
247
|
+
|
|
248
|
+
{showCreateModal && (
|
|
249
|
+
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200">
|
|
250
|
+
<div className="bg-white dark:bg-slate-800 p-6 rounded-xl w-full max-w-md border border-slate-200 dark:border-slate-700 shadow-xl">
|
|
251
|
+
<h2 className="text-xl font-bold mb-4 text-slate-900 dark:text-white">Create New Project</h2>
|
|
252
|
+
<form onSubmit={createProject}>
|
|
253
|
+
<div className="mb-4">
|
|
254
|
+
<label className="block text-sm font-medium mb-1 text-slate-700 dark:text-slate-300">Project Name</label>
|
|
255
|
+
<input
|
|
256
|
+
type="text"
|
|
257
|
+
value={newProjectName}
|
|
258
|
+
onChange={(e) => setNewProjectName(e.target.value)}
|
|
259
|
+
className="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 rounded-lg border border-slate-200 dark:border-slate-600 focus:border-blue-500 outline-none text-slate-900 dark:text-white"
|
|
260
|
+
required
|
|
261
|
+
placeholder="e.g., recommendation-system"
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
<div className="mb-6">
|
|
265
|
+
<label className="block text-sm font-medium mb-1 text-slate-700 dark:text-slate-300">Description</label>
|
|
266
|
+
<textarea
|
|
267
|
+
value={newProjectDesc}
|
|
268
|
+
onChange={(e) => setNewProjectDesc(e.target.value)}
|
|
269
|
+
className="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 rounded-lg border border-slate-200 dark:border-slate-600 focus:border-blue-500 outline-none text-slate-900 dark:text-white"
|
|
270
|
+
rows="3"
|
|
271
|
+
placeholder="Brief description of the project..."
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
274
|
+
<div className="flex justify-end gap-3">
|
|
275
|
+
<button
|
|
276
|
+
type="button"
|
|
277
|
+
onClick={() => setShowCreateModal(false)}
|
|
278
|
+
className="px-4 py-2 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
|
|
279
|
+
>
|
|
280
|
+
Cancel
|
|
281
|
+
</button>
|
|
282
|
+
<Button type="submit">
|
|
283
|
+
Create Project
|
|
284
|
+
</Button>
|
|
285
|
+
</div>
|
|
286
|
+
</form>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|