s11
parent
340a1c3e61
commit
00d2bdd384
|
@ -1,42 +0,0 @@
|
||||||
{
|
|
||||||
"meta": {
|
|
||||||
"expiry": "2025-06-29 11:36:07.506Z",
|
|
||||||
"rawUser": {
|
|
||||||
"email": "suvodipghosh35@gmail.com",
|
|
||||||
"id": "3913660032212288",
|
|
||||||
"name": "Suvodip Ghosh",
|
|
||||||
"picture": {
|
|
||||||
"data": {
|
|
||||||
"height": 201,
|
|
||||||
"is_silhouette": false,
|
|
||||||
"url": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=3913660032212288&height=200&width=200&ext=1748604969&hash=AbaH-X9tWREaBN46FPCtAG6g",
|
|
||||||
"width": 200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"id": "3913660032212288",
|
|
||||||
"name": "Suvodip Ghosh",
|
|
||||||
"username": "",
|
|
||||||
"email": "suvodipghosh35@gmail.com",
|
|
||||||
"avatarURL": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=3913660032212288&height=200&width=200&ext=1748604969&hash=AbaH-X9tWREaBN46FPCtAG6g",
|
|
||||||
"accessToken": "EAATKn4EGuGABO5BQDFZBCNX6QrVyfsUZBCfJKov5OzHHrQIAEu5wOePaLcvIZBklT9MPkoiZCrZCraJNIwtBzGrKxZCeUMwKJRAbicZAPuK1gesmDl27iL3J7vp0sAjMoue4yACIZAIRDZBzsDU0Yi91ztFJ6FOJ2qWVfLkTgjkKwEudTe95vUYcN1E9oS6bo6smA75KdVB6RgGiK217C4ez9aRDrQcvH3ZBSGZBY2QZBW1KynycojdqhKAQS68xT1Bpezv80kEP5DQfONMMNbaUOwBU9r0WxwS0viCbW0qZAk92mXYZAZBQhny5Ili4aP1jmAZD",
|
|
||||||
"refreshToken": "",
|
|
||||||
"avatarUrl": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=3913660032212288&height=200&width=200&ext=1748604969&hash=AbaH-X9tWREaBN46FPCtAG6g"
|
|
||||||
},
|
|
||||||
"record": {
|
|
||||||
"avatar": "colorful_bird_sits_branch_forest_uiu589q5aa.jpg",
|
|
||||||
"collectionId": "_pb_users_auth_",
|
|
||||||
"collectionName": "users",
|
|
||||||
"created": "2025-03-10 13:17:51.909Z",
|
|
||||||
"email": "neha@siliconpin.com",
|
|
||||||
"emailVisibility": true,
|
|
||||||
"id": "7fp08mzhs7qgmg9",
|
|
||||||
"name": "Neha Ghosh",
|
|
||||||
"phone": "9090935312",
|
|
||||||
"provider": "",
|
|
||||||
"type": "admin",
|
|
||||||
"updated": "2025-04-30 09:34:24.970Z",
|
|
||||||
"verified": true
|
|
||||||
},
|
|
||||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NDY2MTc3NzAsImlkIjoiN2ZwMDhtemhzN3FnbWc5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.5Xz_0hoAod9YLgeOTHbWTEKdRxqusZPUDyb2D0SfVKQ"
|
|
||||||
}
|
|
77
github.json
77
github.json
|
@ -1,77 +0,0 @@
|
||||||
{
|
|
||||||
"meta": {
|
|
||||||
"expiry": "",
|
|
||||||
"rawUser": {
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/128118374?v=4",
|
|
||||||
"bio": null,
|
|
||||||
"blog": "",
|
|
||||||
"collaborators": 0,
|
|
||||||
"company": null,
|
|
||||||
"created_at": "2023-03-17T04:49:12Z",
|
|
||||||
"disk_usage": 55453,
|
|
||||||
"email": null,
|
|
||||||
"events_url": "https://api.github.com/users/suvodip35/events{/privacy}",
|
|
||||||
"followers": 0,
|
|
||||||
"followers_url": "https://api.github.com/users/suvodip35/followers",
|
|
||||||
"following": 1,
|
|
||||||
"following_url": "https://api.github.com/users/suvodip35/following{/other_user}",
|
|
||||||
"gists_url": "https://api.github.com/users/suvodip35/gists{/gist_id}",
|
|
||||||
"gravatar_id": "",
|
|
||||||
"hireable": null,
|
|
||||||
"html_url": "https://github.com/suvodip35",
|
|
||||||
"id": 128118374,
|
|
||||||
"location": null,
|
|
||||||
"login": "suvodip35",
|
|
||||||
"name": "Suvodip",
|
|
||||||
"node_id": "U_kgDOB6LuZg",
|
|
||||||
"notification_email": null,
|
|
||||||
"organizations_url": "https://api.github.com/users/suvodip35/orgs",
|
|
||||||
"owned_private_repos": 4,
|
|
||||||
"plan": {
|
|
||||||
"collaborators": 0,
|
|
||||||
"name": "free",
|
|
||||||
"private_repos": 10000,
|
|
||||||
"space": 976562499
|
|
||||||
},
|
|
||||||
"private_gists": 0,
|
|
||||||
"public_gists": 0,
|
|
||||||
"public_repos": 4,
|
|
||||||
"received_events_url": "https://api.github.com/users/suvodip35/received_events",
|
|
||||||
"repos_url": "https://api.github.com/users/suvodip35/repos",
|
|
||||||
"site_admin": false,
|
|
||||||
"starred_url": "https://api.github.com/users/suvodip35/starred{/owner}{/repo}",
|
|
||||||
"subscriptions_url": "https://api.github.com/users/suvodip35/subscriptions",
|
|
||||||
"total_private_repos": 4,
|
|
||||||
"twitter_username": null,
|
|
||||||
"two_factor_authentication": true,
|
|
||||||
"type": "User",
|
|
||||||
"updated_at": "2025-04-30T08:46:03Z",
|
|
||||||
"url": "https://api.github.com/users/suvodip35",
|
|
||||||
"user_view_type": "private"
|
|
||||||
},
|
|
||||||
"id": "128118374",
|
|
||||||
"name": "Suvodip",
|
|
||||||
"username": "suvodip35",
|
|
||||||
"email": "suvodipghosh35@gmail.com",
|
|
||||||
"avatarURL": "https://avatars.githubusercontent.com/u/128118374?v=4",
|
|
||||||
"accessToken": "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",
|
|
||||||
"refreshToken": "",
|
|
||||||
"avatarUrl": "https://avatars.githubusercontent.com/u/128118374?v=4"
|
|
||||||
},
|
|
||||||
"record": {
|
|
||||||
"avatar": "colorful_bird_sits_branch_forest_uiu589q5aa.jpg",
|
|
||||||
"collectionId": "_pb_users_auth_",
|
|
||||||
"collectionName": "users",
|
|
||||||
"created": "2025-03-10 13:17:51.909Z",
|
|
||||||
"email": "neha@siliconpin.com",
|
|
||||||
"emailVisibility": true,
|
|
||||||
"id": "7fp08mzhs7qgmg9",
|
|
||||||
"name": "Neha Ghosh",
|
|
||||||
"phone": "9090935312",
|
|
||||||
"provider": "",
|
|
||||||
"type": "admin",
|
|
||||||
"updated": "2025-04-30 09:34:24.970Z",
|
|
||||||
"verified": true
|
|
||||||
},
|
|
||||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NDY2MTc2NDQsImlkIjoiN2ZwMDhtemhzN3FnbWc5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.XLlZwZ3TrHWlIoxOhkLoKW7AbTPr6VIB49fRpNCTVbY"
|
|
||||||
}
|
|
38
google.json
38
google.json
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"meta": {
|
|
||||||
"expiry": "2025-04-30 12:35:11.096Z",
|
|
||||||
"rawUser": {
|
|
||||||
"email": "suvoairtel@gmail.com",
|
|
||||||
"family_name": "GHOSH",
|
|
||||||
"given_name": "SUBHODIP",
|
|
||||||
"id": "112026834578335832361",
|
|
||||||
"name": "SUBHODIP GHOSH",
|
|
||||||
"picture": "https://lh3.googleusercontent.com/a/ACg8ocLvTh2_Injp6f23coUiAfTQB_WmLki0vmQinLak3pnY_xnznQ=s96-c",
|
|
||||||
"verified_email": true
|
|
||||||
},
|
|
||||||
"id": "112026834578335832361",
|
|
||||||
"name": "SUBHODIP GHOSH",
|
|
||||||
"username": "",
|
|
||||||
"email": "suvoairtel@gmail.com",
|
|
||||||
"avatarURL": "https://lh3.googleusercontent.com/a/ACg8ocLvTh2_Injp6f23coUiAfTQB_WmLki0vmQinLak3pnY_xnznQ=s96-c",
|
|
||||||
"accessToken": "ya29.a0AZYkNZiV6bkpVlDOQy2ayy6WCULOE5uWYi_QvRSN9vUqJgUxuxcn3xlUzbe6rOHQV7UCjPb_aCmiOojHCMWsCvtky80pUBNCI2rGGhCZYjpSMTispb9qvPyO7CNgsqh5YGxTUUXAkuz37uXm68MFv5YOoofMZ1f39f2OU0sIhisaCgYKAbgSARMSFQHGX2MiEUQ1pMH6u7K92FgWTPp4Uw0178",
|
|
||||||
"refreshToken": "",
|
|
||||||
"avatarUrl": "https://lh3.googleusercontent.com/a/ACg8ocLvTh2_Injp6f23coUiAfTQB_WmLki0vmQinLak3pnY_xnznQ=s96-c"
|
|
||||||
},
|
|
||||||
"record": {
|
|
||||||
"avatar": "colorful_bird_sits_branch_forest_uiu589q5aa.jpg",
|
|
||||||
"collectionId": "_pb_users_auth_",
|
|
||||||
"collectionName": "users",
|
|
||||||
"created": "2025-03-10 13:17:51.909Z",
|
|
||||||
"email": "neha@siliconpin.com",
|
|
||||||
"emailVisibility": true,
|
|
||||||
"id": "7fp08mzhs7qgmg9",
|
|
||||||
"name": "Neha Ghosh",
|
|
||||||
"phone": "9090935312",
|
|
||||||
"provider": "",
|
|
||||||
"type": "admin",
|
|
||||||
"updated": "2025-04-30 09:34:24.970Z",
|
|
||||||
"verified": true
|
|
||||||
},
|
|
||||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NDY2MTc3MTMsImlkIjoiN2ZwMDhtemhzN3FnbWc5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.Wig_LpSxOd03z389n186t2AwUu1Ypxmj3Apf5LdciaQ"
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -21,6 +21,7 @@
|
||||||
"@shadcn/ui": "^0.0.4",
|
"@shadcn/ui": "^0.0.4",
|
||||||
"@types/date-fns": "^2.5.3",
|
"@types/date-fns": "^2.5.3",
|
||||||
"@types/react": "^19.0.12",
|
"@types/react": "^19.0.12",
|
||||||
|
"@uiw/react-markdown-preview": "^5.1.4",
|
||||||
"@uiw/react-md-editor": "^3.25.6",
|
"@uiw/react-md-editor": "^3.25.6",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"astro": "^5.5.2",
|
"astro": "^5.5.2",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" aria-label="Gitea" role="img" viewBox="0 0 512 512" width="40px" height="40px" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <rect rx="15%" height="512" width="512" fill="#ffffff"></rect> <path d="M419 150c-98 7-186 2-276-1-27 0-63 19-61 67 3 75 71 82 99 83 3 14 35 62 59 65h104c63-5 109-213 75-214zm-311 67c-3-21 7-42 42-42 3 39 10 61 22 96-32-5-59-15-64-54z" fill="#592"></path> <path d="m293 152v70" stroke="#ffffff" stroke-width="9"></path> <g transform="rotate(25.7 496 -423)" stroke-width="7" fill="#592"> <path d="M561 246h97" stroke="#592"></path> <rect x="561" y="246" width="97" height="97" rx="16" fill="#ffffff"></rect> <path d="M592 245v75" stroke="#592"></path> <path d="M592 273c45 0 38-5 38 48" fill="none" stroke="#592"></path> <circle cx="592" cy="320" r="10"></circle> <circle cx="630" cy="320" r="10"></circle> <circle cx="592" cy="273" r="10"></circle> </g> </g></svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1,22 @@
|
||||||
|
let endpoint = 'https://api.utho.com/v2/kubernetes/deploy';
|
||||||
|
let kuberPayload = {
|
||||||
|
"dcslug": "inmumbaizone2",
|
||||||
|
"cluster_label": "MyK8S-flZuUYtk-nvn9h",
|
||||||
|
"cluster_version": "1.30.0-utho",
|
||||||
|
"nodepools": [
|
||||||
|
{
|
||||||
|
"label": "pool-GPlb5CRP",
|
||||||
|
"size": "10215",
|
||||||
|
"count": "2",
|
||||||
|
"ebs": [
|
||||||
|
{
|
||||||
|
"disk": "30",
|
||||||
|
"type": "nvme"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vpc": "81b2bd94-61dc-424b-a1ca-ca4c810ed4c4",
|
||||||
|
"network_type": "publicprivate",
|
||||||
|
"cpumodel": "amd"
|
||||||
|
}
|
|
@ -0,0 +1,470 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
||||||
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import { Label } from "../ui/label";
|
||||||
|
import Loader from "../ui/loader";
|
||||||
|
import { useToast } from "../ui/toast";
|
||||||
|
import { useIsLoggedIn } from '../../lib/isLoggedIn';
|
||||||
|
|
||||||
|
export default function Kubernetes() {
|
||||||
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isFetchingData, setIsFetchingData] = useState(true);
|
||||||
|
const [deployError, setDeployError] = useState(null);
|
||||||
|
const [deployStatus, setDeployStatus] = useState({});
|
||||||
|
const [vpcs, setVpcs] = useState([]);
|
||||||
|
const [nodePools, setNodePools] = useState([{ label: '', size: '', count: 1, ebs: [{ disk: '', type: 'nvme' }] }]);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
dcslug: 'inmumbaizone2',
|
||||||
|
cluster_label: '',
|
||||||
|
cluster_version: '1.30.0-utho',
|
||||||
|
vpc: '',
|
||||||
|
network_type: 'publicprivate',
|
||||||
|
cpumodel: 'amd'
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableSizes = [
|
||||||
|
{ id: '10215', name: '2 vCPU, 4GB RAM' },
|
||||||
|
{ id: '10216', name: '4 vCPU, 8GB RAM' },
|
||||||
|
{ id: '10217', name: '8 vCPU, 16GB RAM' },
|
||||||
|
{ id: '10218', name: '16 vCPU, 32GB RAM' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const availableVersions = [
|
||||||
|
'1.30.0-utho',
|
||||||
|
'1.29.0-utho',
|
||||||
|
'1.28.0-utho'
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
// Simulate fetching VPCs - replace with actual API call
|
||||||
|
const mockVpcs = [
|
||||||
|
{ id: '81b2bd94-61dc-424b-a1ca-ca4c810ed4c4', name: 'Default VPC (Mumbai)' },
|
||||||
|
{ id: 'c17032c9-3cfd-4028-8f2a-f3f5aa8c2976', name: 'Default VPC (Bangalore)' }
|
||||||
|
];
|
||||||
|
setVpcs(mockVpcs);
|
||||||
|
} catch (error) {
|
||||||
|
showToast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load configuration data",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsFetchingData(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
nodepools: nodePools.map(pool => ({
|
||||||
|
...pool,
|
||||||
|
count: String(pool.count), // Ensure count is string as per API
|
||||||
|
ebs: pool.ebs.map(disk => ({
|
||||||
|
...disk,
|
||||||
|
disk: String(disk.disk) // Ensure disk size is string
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch("https://host-api.cs1.hz.siliconpin.com/v1/kubernetis/?query=deploy&source=chanel_1", {
|
||||||
|
credentials: "include",
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === "success") {
|
||||||
|
setDeployStatus({
|
||||||
|
status: data.status,
|
||||||
|
clusterId: data.clusterId,
|
||||||
|
endpoint: data.endpoint
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setDeployError(error.message);
|
||||||
|
showToast({
|
||||||
|
title: "Deployment Error",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodePoolChange = (index, field, value) => {
|
||||||
|
const updatedPools = [...nodePools];
|
||||||
|
updatedPools[index][field] = value;
|
||||||
|
setNodePools(updatedPools);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDiskChange = (poolIndex, diskIndex, field, value) => {
|
||||||
|
const updatedPools = [...nodePools];
|
||||||
|
updatedPools[poolIndex].ebs[diskIndex][field] = value;
|
||||||
|
setNodePools(updatedPools);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addNodePool = () => {
|
||||||
|
setNodePools([...nodePools, { label: '', size: '', count: 1, ebs: [{ disk: '', type: 'nvme' }] }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeNodePool = (index) => {
|
||||||
|
if (nodePools.length > 1) {
|
||||||
|
const updatedPools = [...nodePools];
|
||||||
|
updatedPools.splice(index, 1);
|
||||||
|
setNodePools(updatedPools);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDisk = (poolIndex) => {
|
||||||
|
const updatedPools = [...nodePools];
|
||||||
|
updatedPools[poolIndex].ebs.push({ disk: '', type: 'nvme' });
|
||||||
|
setNodePools(updatedPools);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDisk = (poolIndex, diskIndex) => {
|
||||||
|
const updatedPools = [...nodePools];
|
||||||
|
if (updatedPools[poolIndex].ebs.length > 1) {
|
||||||
|
updatedPools[poolIndex].ebs.splice(diskIndex, 1);
|
||||||
|
setNodePools(updatedPools);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading || (isLoggedIn && isFetchingData)) {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deployError) return <p>Error: {deployError}</p>;
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
return <p className="text-center mt-8">
|
||||||
|
You are not Logged in. <a href="/login" className="text-[#6d9e37]">Click Here</a> to login first then you can add this service.
|
||||||
|
</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deployStatus.status === 'success') {
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-2xl mx-auto my-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Kubernetes Cluster Deployed Successfully</CardTitle>
|
||||||
|
<CardDescription>Your Kubernetes cluster is now being provisioned.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>Cluster ID:</Label>
|
||||||
|
<p className="font-mono p-2 bg-gray-100 rounded">{deployStatus.clusterId}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>API Endpoint:</Label>
|
||||||
|
<p className="font-mono p-2 bg-gray-100 rounded">{deployStatus.endpoint}</p>
|
||||||
|
</div>
|
||||||
|
<div className="pt-4">
|
||||||
|
<Button onClick={() => window.location.href = '/services/kubernetes'}>
|
||||||
|
Back to Kubernetes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-2xl mx-auto my-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Create New Kubernetes Cluster</CardTitle>
|
||||||
|
<CardDescription>Configure your Kubernetes cluster with the desired node pools and settings.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cluster_label">Cluster Name *</Label>
|
||||||
|
<Input
|
||||||
|
id="cluster_label"
|
||||||
|
name="cluster_label"
|
||||||
|
value={formData.cluster_label}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="my-cluster"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="dcslug">Data Center *</Label>
|
||||||
|
<Select
|
||||||
|
name="dcslug"
|
||||||
|
value={formData.dcslug}
|
||||||
|
onValueChange={(value) => setFormData({...formData, dcslug: value})}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select location" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="inmumbaizone2">Mumbai</SelectItem>
|
||||||
|
<SelectItem value="inbangalore">Bangalore</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cluster_version">Kubernetes Version *</Label>
|
||||||
|
<Select
|
||||||
|
name="cluster_version"
|
||||||
|
value={formData.cluster_version}
|
||||||
|
onValueChange={(value) => setFormData({...formData, cluster_version: value})}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select version" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableVersions.map(version => (
|
||||||
|
<SelectItem key={version} value={version}>
|
||||||
|
{version}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="vpc">VPC *</Label>
|
||||||
|
<Select
|
||||||
|
name="vpc"
|
||||||
|
value={formData.vpc}
|
||||||
|
onValueChange={(value) => setFormData({...formData, vpc: value})}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select VPC" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{vpcs.map(vpc => (
|
||||||
|
<SelectItem key={vpc.id} value={vpc.id}>
|
||||||
|
{vpc.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Network Type *</Label>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="network_type"
|
||||||
|
value="publicprivate"
|
||||||
|
checked={formData.network_type === 'publicprivate'}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="text-[#6d9e37]"
|
||||||
|
/>
|
||||||
|
<span>Public + Private</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="network_type"
|
||||||
|
value="private"
|
||||||
|
checked={formData.network_type === 'private'}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="text-[#6d9e37]"
|
||||||
|
/>
|
||||||
|
<span>Private Only</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>CPU Model *</Label>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="cpumodel"
|
||||||
|
value="amd"
|
||||||
|
checked={formData.cpumodel === 'amd'}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="text-[#6d9e37]"
|
||||||
|
/>
|
||||||
|
<span>AMD</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="cpumodel"
|
||||||
|
value="intel"
|
||||||
|
checked={formData.cpumodel === 'intel'}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="text-[#6d9e37]"
|
||||||
|
/>
|
||||||
|
<span>Intel</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Label className="text-lg">Node Pools</Label>
|
||||||
|
<Button type="button" onClick={addNodePool} variant="outline">
|
||||||
|
Add Node Pool
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{nodePools.map((pool, poolIndex) => (
|
||||||
|
<div key={poolIndex} className="p-4 border rounded-lg space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h4 className="font-medium">Node Pool #{poolIndex + 1}</h4>
|
||||||
|
{nodePools.length > 1 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeNodePool(poolIndex)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Pool Label</Label>
|
||||||
|
<Input
|
||||||
|
value={pool.label}
|
||||||
|
onChange={(e) => handleNodePoolChange(poolIndex, 'label', e.target.value)}
|
||||||
|
placeholder="worker-pool"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Node Size *</Label>
|
||||||
|
<Select
|
||||||
|
value={pool.size}
|
||||||
|
onValueChange={(value) => handleNodePoolChange(poolIndex, 'size', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select size" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableSizes.map(size => (
|
||||||
|
<SelectItem key={size.id} value={size.id}>
|
||||||
|
{size.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Node Count *</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={pool.count}
|
||||||
|
onChange={(e) => handleNodePoolChange(poolIndex, 'count', parseInt(e.target.value) || 1)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Label className="text-md">EBS Volumes</Label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => addDisk(poolIndex)}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Add Disk
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pool.ebs.map((disk, diskIndex) => (
|
||||||
|
<div key={diskIndex} className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Disk Size (GB) *</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={disk.disk}
|
||||||
|
onChange={(e) => handleDiskChange(poolIndex, diskIndex, 'disk', e.target.value)}
|
||||||
|
placeholder="30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Disk Type *</Label>
|
||||||
|
<Select
|
||||||
|
value={disk.type}
|
||||||
|
onValueChange={(value) => handleDiskChange(poolIndex, diskIndex, 'type', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="nvme">NVMe SSD</SelectItem>
|
||||||
|
<SelectItem value="ssd">Standard SSD</SelectItem>
|
||||||
|
<SelectItem value="hdd">HDD</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
{pool.ebs.length > 1 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeDisk(poolIndex, diskIndex)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-4">
|
||||||
|
<Button type="submit" disabled={isLoading} className="w-full md:w-auto">
|
||||||
|
{isLoading ? "Deploying Cluster..." : "Deploy Kubernetes Cluster"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,487 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
||||||
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import { Label } from "../ui/label";
|
||||||
|
import Loader from "../ui/loader";
|
||||||
|
import { useToast } from "../ui/toast";
|
||||||
|
import { useIsLoggedIn } from '../../lib/isLoggedIn';
|
||||||
|
|
||||||
|
export default function Kubernetes() {
|
||||||
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isFetchingData, setIsFetchingData] = useState(true);
|
||||||
|
const [deployError, setDeployError] = useState(null);
|
||||||
|
const [deployStatus, setDeployStatus] = useState({});
|
||||||
|
const [vpcs, setVpcs] = useState([]);
|
||||||
|
const [nodePools, setNodePools] = useState([{ label: `${getRandomString()}`, size: '10215', count: 1, ebs: [{ disk: 5, type: 'nvme' }] }]);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({ dcslug: 'inmumbaizone2', cluster_label: `${getRandomString()}`, cluster_version: '1.30.0-utho', network_type: 'publicprivate', });
|
||||||
|
function getRandomString( length = 10, { includeNumbers = true, includeLowercase = true, includeUppercase = true, } = {} ) {
|
||||||
|
let chars = '';
|
||||||
|
const numbers = '0123456789';
|
||||||
|
const lowercase = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
|
||||||
|
if (includeNumbers) chars += numbers;
|
||||||
|
if (includeLowercase) chars += lowercase;
|
||||||
|
if (includeUppercase) chars += uppercase;
|
||||||
|
|
||||||
|
if (!chars) {
|
||||||
|
throw new Error('At least one character type must be included');
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const availableSizes = [
|
||||||
|
{ id: '10215', name: '2 vCPU, 4GB RAM' },
|
||||||
|
{ id: '10216', name: '4 vCPU, 8GB RAM' },
|
||||||
|
{ id: '10217', name: '8 vCPU, 16GB RAM' },
|
||||||
|
{ id: '10218', name: '16 vCPU, 32GB RAM' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const availableVersions = [
|
||||||
|
'1.30.0-utho',
|
||||||
|
'1.29.0-utho',
|
||||||
|
'1.28.0-utho'
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
// Simulate fetching VPCs - replace with actual API call
|
||||||
|
const mockVpcs = [
|
||||||
|
{ id: '81b2bd94-61dc-424b-a1ca-ca4c810ed4c4', name: 'Default VPC (Mumbai)' },
|
||||||
|
{ id: 'c17032c9-3cfd-4028-8f2a-f3f5aa8c2976', name: 'Default VPC (Bangalore)' }
|
||||||
|
];
|
||||||
|
setVpcs(mockVpcs);
|
||||||
|
} catch (error) {
|
||||||
|
showToast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load configuration data",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsFetchingData(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
nodepools: nodePools.map(pool => ({
|
||||||
|
...pool,
|
||||||
|
count: String(pool.count), // Ensure count is string as per API
|
||||||
|
ebs: pool.ebs.map(disk => ({
|
||||||
|
...disk,
|
||||||
|
disk: String(disk.disk) // Ensure disk size is string
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch("https://host-api.cs1.hz.siliconpin.com/v1/kubernetis/?query=deploy&source=chanel_1", {
|
||||||
|
credentials: "include",
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === "success") {
|
||||||
|
setDeployStatus({
|
||||||
|
status: data.status,
|
||||||
|
clusterId: data.clusterId,
|
||||||
|
endpoint: data.endpoint
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setDeployError(error.message);
|
||||||
|
showToast({
|
||||||
|
title: "Deployment Error",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodePoolChange = (index, field, value) => {
|
||||||
|
const updatedPools = [...nodePools];
|
||||||
|
updatedPools[index][field] = value;
|
||||||
|
setNodePools(updatedPools);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDiskChange = (poolIndex, diskIndex, field, value) => {
|
||||||
|
const updatedPools = [...nodePools];
|
||||||
|
updatedPools[poolIndex].ebs[diskIndex][field] = value;
|
||||||
|
setNodePools(updatedPools);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addNodePool = () => {
|
||||||
|
setNodePools([...nodePools, { label: '', size: '', count: 1, ebs: [{ disk: '', type: 'nvme' }] }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeNodePool = (index) => {
|
||||||
|
if (nodePools.length > 1) {
|
||||||
|
const updatedPools = [...nodePools];
|
||||||
|
updatedPools.splice(index, 1);
|
||||||
|
setNodePools(updatedPools);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDisk = (poolIndex) => {
|
||||||
|
const updatedPools = [...nodePools];
|
||||||
|
updatedPools[poolIndex].ebs.push({ disk: '', type: 'nvme' });
|
||||||
|
setNodePools(updatedPools);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDisk = (poolIndex, diskIndex) => {
|
||||||
|
const updatedPools = [...nodePools];
|
||||||
|
if (updatedPools[poolIndex].ebs.length > 1) {
|
||||||
|
updatedPools[poolIndex].ebs.splice(diskIndex, 1);
|
||||||
|
setNodePools(updatedPools);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading || (isLoggedIn && isFetchingData)) {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deployError) return <p>Error: {deployError}</p>;
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
return <p className="text-center mt-8">
|
||||||
|
You are not Logged in. <a href="/login" className="text-[#6d9e37]">Click Here</a> to login first then you can add this service.
|
||||||
|
</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deployStatus.status === 'success') {
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-2xl mx-auto my-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Kubernetes Cluster Deployed Successfully</CardTitle>
|
||||||
|
<CardDescription>Your Kubernetes cluster is now being provisioned.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>Cluster ID:</Label>
|
||||||
|
<p className="font-mono p-2 bg-gray-100 rounded">{deployStatus.id}</p>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
{/* <div>
|
||||||
|
<Label>API Endpoint:</Label>
|
||||||
|
<p className="font-mono p-2 bg-gray-100 rounded">{deployStatus.endpoint}</p>
|
||||||
|
</div> */}
|
||||||
|
<div className="pt-4">
|
||||||
|
<Button onClick={() => window.location.href = '/services/kubernetis'}>
|
||||||
|
Back to Kubernetes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-2xl mx-auto my-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Create New Kubernetes Cluster</CardTitle>
|
||||||
|
<CardDescription>Configure your Kubernetes cluster with the desired node pools and settings.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cluster_label">Cluster Name *</Label>
|
||||||
|
<Input
|
||||||
|
id="cluster_label"
|
||||||
|
name="cluster_label"
|
||||||
|
value={formData.cluster_label}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="my-cluster"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className="space-y-2">
|
||||||
|
<Label htmlFor="dcslug">Data Center *</Label>
|
||||||
|
<Select
|
||||||
|
name="dcslug"
|
||||||
|
value={formData.dcslug}
|
||||||
|
onValueChange={(value) => setFormData({...formData, dcslug: value})}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select location" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem selected value="inmumbaizone2">Mumbai</SelectItem>
|
||||||
|
<SelectItem value="inbangalore">Bangalore</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cluster_version">Kubernetes Version *</Label>
|
||||||
|
<Select
|
||||||
|
name="cluster_version"
|
||||||
|
value={formData.cluster_version}
|
||||||
|
onValueChange={(value) => setFormData({...formData, cluster_version: value})}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select version" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableVersions.map(version => (
|
||||||
|
<SelectItem key={version} value={version}>
|
||||||
|
{version}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className="space-y-2">
|
||||||
|
<Label htmlFor="vpc">VPC *</Label>
|
||||||
|
<Select
|
||||||
|
name="vpc"
|
||||||
|
value={formData.vpc}
|
||||||
|
onValueChange={(value) => setFormData({...formData, vpc: value})}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select VPC" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{vpcs.map(vpc => (
|
||||||
|
<SelectItem key={vpc.id} value={vpc.id}>
|
||||||
|
{vpc.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Network Type *</Label>
|
||||||
|
<div className="flex gap-4 border border-neutral-600 rounded-md p-2">
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="network_type"
|
||||||
|
value="publicprivate"
|
||||||
|
checked={formData.network_type === 'publicprivate'}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="text-[#6d9e37] accent-[#6d9e37]"
|
||||||
|
/>
|
||||||
|
<span>Public + Private</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="network_type"
|
||||||
|
value="private"
|
||||||
|
checked={formData.network_type === 'private'}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="text-[#6d9e37] accent-[#6d9e37]"
|
||||||
|
/>
|
||||||
|
<span>Private Only</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* <div className="space-y-2">
|
||||||
|
<Label>CPU Model *</Label>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="cpumodel"
|
||||||
|
value="amd"
|
||||||
|
checked={formData.cpumodel === 'amd'}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="text-[#6d9e37]"
|
||||||
|
/>
|
||||||
|
<span>AMD</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="cpumodel"
|
||||||
|
value="intel"
|
||||||
|
checked={formData.cpumodel === 'intel'}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="text-[#6d9e37]"
|
||||||
|
/>
|
||||||
|
<span>Intel</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Label className="text-lg">Node Pools</Label>
|
||||||
|
<Button type="button" onClick={addNodePool} variant="outline">
|
||||||
|
Add Node Pool
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{nodePools.map((pool, poolIndex) => (
|
||||||
|
<div key={poolIndex} className="p-4 border rounded-lg space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h4 className="font-medium">Node Pool #{poolIndex + 1}</h4>
|
||||||
|
{nodePools.length > 1 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeNodePool(poolIndex)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Pool Label</Label>
|
||||||
|
<Input
|
||||||
|
value={pool.label}
|
||||||
|
onChange={(e) => handleNodePoolChange(poolIndex, 'label', e.target.value)}
|
||||||
|
placeholder="worker-pool"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Node Size *</Label>
|
||||||
|
<Select
|
||||||
|
value={pool.size}
|
||||||
|
onValueChange={(value) => handleNodePoolChange(poolIndex, 'size', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select size" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableSizes.map((size, index) => (
|
||||||
|
<SelectItem selected={index === 0} key={size.id} value={size.id}>
|
||||||
|
{size.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Node Count *</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={pool.count}
|
||||||
|
onChange={(e) => handleNodePoolChange(poolIndex, 'count', parseInt(e.target.value) || 1)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Label className="text-md">EBS Volumes</Label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => addDisk(poolIndex)}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Add Disk
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pool.ebs.map((disk, diskIndex) => (
|
||||||
|
<div key={diskIndex} className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Disk Size (GB) *</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={disk.disk}
|
||||||
|
onChange={(e) => handleDiskChange(poolIndex, diskIndex, 'disk', e.target.value)}
|
||||||
|
placeholder="30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Disk Type *</Label>
|
||||||
|
<Select
|
||||||
|
value={disk.type}
|
||||||
|
onValueChange={(value) => handleDiskChange(poolIndex, diskIndex, 'type', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="nvme">NVMe SSD</SelectItem>
|
||||||
|
<SelectItem value="ssd">Standard SSD</SelectItem>
|
||||||
|
<SelectItem value="hdd">HDD</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
{pool.ebs.length > 1 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeDisk(poolIndex, diskIndex)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-4">
|
||||||
|
<Button type="submit" disabled={isLoading} className="w-full md:w-auto">
|
||||||
|
{isLoading ? "Deploying Cluster..." : "Deploy Kubernetes Cluster"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
|
@ -6,477 +6,462 @@ import { Input } from "../ui/input";
|
||||||
import { Label } from "../ui/label";
|
import { Label } from "../ui/label";
|
||||||
import Loader from "../ui/loader";
|
import Loader from "../ui/loader";
|
||||||
import { useToast } from "../ui/toast";
|
import { useToast } from "../ui/toast";
|
||||||
import { Switch } from "../ui/switch";
|
import { useIsLoggedIn } from '../../lib/isLoggedIn';
|
||||||
|
|
||||||
export default function NewKubernetesService() {
|
export default function Kubernetes() {
|
||||||
|
const { isLoggedIn, loading } = useIsLoggedIn();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isFetchingData, setIsFetchingData] = useState(true);
|
const [deployError, setDeployError] = useState(null);
|
||||||
const [plans, setPlans] = useState([]);
|
const [deployStatus, setDeployStatus] = useState({});
|
||||||
const [vpcs, setVpcs] = useState([]);
|
const [clusterStatus, setClusterStatus] = useState('');
|
||||||
const [subnets, setSubnets] = useState([]);
|
const [nodePools, setNodePools] = useState([{
|
||||||
const PUBLIC_UTHO_API_KEY = import.meta.env.PUBLIC_UTHO_API_KEY;
|
label: `${getRandomString()}`,
|
||||||
|
size: '10215',
|
||||||
|
count: 1,
|
||||||
|
ebs: [{ disk: 30, type: 'nvme' }]
|
||||||
|
}]);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
dcslug: "innoida",
|
dcslug: 'inmumbaizone2',
|
||||||
cluster_version: "1.24",
|
cluster_label: `${getRandomString()}`,
|
||||||
cluster_label: "",
|
|
||||||
nodepools: [{
|
|
||||||
label: "default-pool",
|
|
||||||
size: "",
|
|
||||||
count: 1,
|
|
||||||
maxCount: 1
|
|
||||||
}],
|
|
||||||
firewall: "",
|
|
||||||
vpc: "",
|
|
||||||
subnet: "",
|
|
||||||
network_type: "public",
|
|
||||||
cpumodel: "intel"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Generate random string for IDs
|
||||||
|
function getRandomString(length = 8) {
|
||||||
|
return Math.random().toString(36).substring(2, length+2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableSizes = [
|
||||||
|
{ id: '10215', name: '2 vCPU, 4GB RAM' },
|
||||||
|
{ id: '10216', name: '4 vCPU, 8GB RAM' },
|
||||||
|
{ id: '10217', name: '8 vCPU, 16GB RAM' },
|
||||||
|
{ id: '10218', name: '16 vCPU, 32GB RAM' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check cluster status periodically after deployment
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchInitialData = async () => {
|
if (!deployStatus.clusterId || deployStatus.isReady) return;
|
||||||
setIsFetchingData(true);
|
|
||||||
|
const checkStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const [plansResponse, vpcsResponse] = await Promise.all([
|
const response = await fetch(
|
||||||
fetch("https://api.utho.com/v2/plans", {
|
`https://host-api.cs1.hz.siliconpin.com/v1/kubernetis/?query=status&clusterId=${deployStatus.clusterId}&source=chanel_1`,
|
||||||
headers: {
|
{ credentials: "include" }
|
||||||
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
);
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
fetch("https://api.utho.com/v2/vpc", {
|
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
|
|
||||||
const plansData = await plansResponse.json();
|
if (!response.ok) throw new Error("Failed to check status");
|
||||||
const vpcsData = await vpcsResponse.json();
|
|
||||||
|
|
||||||
setPlans(plansData.plans || []);
|
const data = await response.json();
|
||||||
setVpcs(vpcsData.vpc || []);
|
|
||||||
|
|
||||||
// Set default VPC if available
|
// Update status
|
||||||
if (vpcsData.vpc?.length > 0) {
|
setClusterStatus(data.status || 'pending');
|
||||||
const firstVpc = vpcsData.vpc[0];
|
|
||||||
setFormData(prev => ({
|
if (data.status === 'active') {
|
||||||
...prev,
|
// Cluster is ready for configuration download
|
||||||
vpc: firstVpc.id
|
setDeployStatus(prev => ({ ...prev, isReady: true }));
|
||||||
}));
|
showToast({
|
||||||
// Fetch subnets for the default VPC
|
title: "Cluster Ready",
|
||||||
await fetchSubnets(firstVpc.id);
|
description: "Your Kubernetes cluster is now fully configured.",
|
||||||
|
variant: "default"
|
||||||
|
});
|
||||||
|
return; // Stop checking when ready
|
||||||
|
} else if (data.status === 'failed') {
|
||||||
|
throw new Error("Cluster deployment failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Continue checking every 20 seconds if not ready
|
||||||
|
setTimeout(checkStatus, 20000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Initial data fetch error:", error);
|
console.error("Status check error:", error);
|
||||||
showToast({
|
// Retry after delay if not a fatal error
|
||||||
title: "Error",
|
if (!error.message.includes('failed')) {
|
||||||
description: "Failed to load initial configuration data",
|
setTimeout(checkStatus, 30000);
|
||||||
variant: "destructive"
|
} else {
|
||||||
});
|
setDeployError(error.message);
|
||||||
} finally {
|
}
|
||||||
setIsFetchingData(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchInitialData();
|
const timer = setTimeout(checkStatus, 15000);
|
||||||
}, []);
|
return () => clearTimeout(timer);
|
||||||
|
}, [deployStatus.clusterId, deployStatus.isReady]);
|
||||||
const fetchSubnets = async (vpcId) => {
|
|
||||||
try {
|
|
||||||
// First try to get subnets from the VPC data
|
|
||||||
const selectedVpc = vpcs.find(vpc => vpc.id === vpcId);
|
|
||||||
if (selectedVpc?.subnet?.length > 0) {
|
|
||||||
setSubnets(selectedVpc.subnet);
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
subnet: selectedVpc.subnet[0].id
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no subnets in VPC data, try the subnets endpoint
|
|
||||||
const response = await fetch(`https://api.utho.com/v2/vpc/${vpcId}/subnets`, {
|
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
setSubnets(data.subnets || []);
|
|
||||||
if (data.subnets?.length > 0) {
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
subnet: data.subnets[0].id
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching subnets:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVpcChange = async (vpcId) => {
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
vpc: vpcId,
|
|
||||||
subnet: ""
|
|
||||||
}));
|
|
||||||
await fetchSubnets(vpcId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
setDeployError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate required fields including subnet
|
|
||||||
if (!formData.cluster_label || !formData.nodepools[0].size || !formData.vpc || !formData.subnet) {
|
|
||||||
throw new Error("Please fill all required fields including VPC and Subnet");
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
dcslug: formData.dcslug,
|
...formData,
|
||||||
cluster_version: formData.cluster_version,
|
cluster_version: '1.30.0-utho',
|
||||||
cluster_label: formData.cluster_label,
|
nodepools: nodePools.map(pool => ({
|
||||||
nodepools: formData.nodepools.map(pool => ({
|
...pool,
|
||||||
label: pool.label,
|
count: String(pool.count),
|
||||||
size: pool.size,
|
ebs: pool.ebs.map(disk => ({
|
||||||
count: pool.count.toString(),
|
...disk,
|
||||||
maxCount: pool.maxCount.toString()
|
disk: String(disk.disk)
|
||||||
|
}))
|
||||||
})),
|
})),
|
||||||
firewall: formData.firewall || undefined,
|
vpc: '81b2bd94-61dc-424b-a1ca-ca4c810ed4c4',
|
||||||
vpc: formData.vpc,
|
network_type: 'publicprivate',
|
||||||
subnet: formData.subnet, // Now required based on API behavior
|
cpumodel: 'amd'
|
||||||
network_type: formData.network_type,
|
|
||||||
cpumodel: formData.cpumodel
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Deployment payload:", payload);
|
const response = await fetch(
|
||||||
|
"https://host-api.cs1.hz.siliconpin.com/v1/kubernetis/?query=deploy&source=chanel_1",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const response = await fetch("https://api.utho.com/v2/kubernetes/deploy", {
|
if (!response.ok) throw new Error("Deployment request failed");
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.status === "success") {
|
if (data.status === "success") {
|
||||||
showToast({
|
setDeployStatus({
|
||||||
title: "Success",
|
status: "success",
|
||||||
description: `Cluster ${data.cluster_id || data.id} is being deployed`,
|
clusterId: data.clusterId,
|
||||||
variant: "success"
|
endpoint: data.endpoint,
|
||||||
|
isReady: false
|
||||||
|
});
|
||||||
|
showToast({
|
||||||
|
title: "Deployment Started",
|
||||||
|
description: "Your cluster is being provisioned. This may take 10-15 minutes.",
|
||||||
|
variant: "default"
|
||||||
});
|
});
|
||||||
// Reset form
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
cluster_label: "",
|
|
||||||
nodepools: [{
|
|
||||||
label: "default-pool",
|
|
||||||
size: "",
|
|
||||||
count: 1,
|
|
||||||
maxCount: 1
|
|
||||||
}],
|
|
||||||
vpc: vpcs[0]?.id || "",
|
|
||||||
subnet: subnets[0]?.id || ""
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
|
throw new Error(data.message || "Cluster deployment failed");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
setDeployError(error.message);
|
||||||
showToast({
|
showToast({
|
||||||
title: "Deployment Failed",
|
title: "Deployment Failed",
|
||||||
description: error.message,
|
description: error.message,
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
console.error("Deployment error:", error);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const downloadKubeConfig = async () => {
|
||||||
|
if (!deployStatus.clusterId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Trigger download via backend
|
||||||
|
window.open(
|
||||||
|
`https://host-api.cs1.hz.siliconpin.com/v1/kubernetis/?query=download&clusterId=${deployStatus.clusterId}&source=chanel_1`,
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
showToast({
|
||||||
|
title: "Download Failed",
|
||||||
|
description: "Could not download configuration. Please try again.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Form field handlers
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
const { name, value, type, checked } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
...prev,
|
|
||||||
[name]: type === "checkbox" ? checked : value
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNodePoolChange = (index, field, value) => {
|
const handleNodePoolChange = (index, field, value) => {
|
||||||
const updatedNodePools = [...formData.nodepools];
|
const updatedPools = [...nodePools];
|
||||||
updatedNodePools[index][field] = value;
|
updatedPools[index][field] = value;
|
||||||
setFormData(prev => ({
|
setNodePools(updatedPools);
|
||||||
...prev,
|
|
||||||
nodepools: updatedNodePools
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isFetchingData) {
|
const handleDiskChange = (poolIndex, diskIndex, field, value) => {
|
||||||
|
const updatedPools = [...nodePools];
|
||||||
|
updatedPools[poolIndex].ebs[diskIndex][field] = value;
|
||||||
|
setNodePools(updatedPools);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addNodePool = () => {
|
||||||
|
setNodePools([...nodePools, {
|
||||||
|
label: `${getRandomString()}`,
|
||||||
|
size: '10215',
|
||||||
|
count: 1,
|
||||||
|
ebs: [{ disk: 30, type: 'nvme' }]
|
||||||
|
}]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeNodePool = (index) => {
|
||||||
|
if (nodePools.length > 1) {
|
||||||
|
const updatedPools = [...nodePools];
|
||||||
|
updatedPools.splice(index, 1);
|
||||||
|
setNodePools(updatedPools);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDisk = (poolIndex) => {
|
||||||
|
const updatedPools = [...nodePools];
|
||||||
|
updatedPools[poolIndex].ebs.push({ disk: 30, type: 'nvme' });
|
||||||
|
setNodePools(updatedPools);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDisk = (poolIndex, diskIndex) => {
|
||||||
|
const updatedPools = [...nodePools];
|
||||||
|
if (updatedPools[poolIndex].ebs.length > 1) {
|
||||||
|
updatedPools[poolIndex].ebs.splice(diskIndex, 1);
|
||||||
|
setNodePools(updatedPools);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<p className="text-center mt-8">
|
||||||
<Loader />
|
You must be logged in to deploy clusters. <a href="/login" className="text-[#6d9e37]">Login here</a>.
|
||||||
<span className="ml-2">Loading configuration...</span>
|
</p>
|
||||||
</div>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deployStatus.status === 'success') {
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-2xl mx-auto my-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Kubernetes Cluster Deployment</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{deployStatus.isReady
|
||||||
|
? "Your cluster is ready to use"
|
||||||
|
: "Your cluster is being provisioned"}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Current Status</Label>
|
||||||
|
<div className="p-3 bg-gray-100 rounded-md">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{!deployStatus.isReady && <Loader size="sm" />}
|
||||||
|
<span className="font-medium">
|
||||||
|
{deployStatus.isReady ? 'Ready' : (clusterStatus || 'Starting deployment')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{!deployStatus.isReady && (
|
||||||
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
|
This process may take several minutes. Please don't close this page.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>Cluster ID</Label>
|
||||||
|
<p className="font-mono p-2 bg-gray-100 rounded text-sm">
|
||||||
|
{deployStatus.clusterId}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>API Endpoint</Label>
|
||||||
|
<p className="font-mono p-2 bg-gray-100 rounded text-sm">
|
||||||
|
{deployStatus.endpoint || 'Pending...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{deployStatus.isReady ? (
|
||||||
|
<div className="flex flex-col gap-4 pt-4">
|
||||||
|
<Button onClick={downloadKubeConfig}>
|
||||||
|
Download Configuration File
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={() => window.location.reload()}>
|
||||||
|
Deploy Another Cluster
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Loader size="sm" />
|
||||||
|
<span>Preparing your cluster... This may take several minutes</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-2xl mx-auto my-4">
|
<Card className="w-full max-w-2xl mx-auto my-4">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Deploy New Kubernetes Cluster</CardTitle>
|
<CardTitle>Create Kubernetes Cluster</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Configure your Kubernetes cluster with the required parameters
|
Configure your Kubernetes cluster with the desired specifications
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{/* Cluster Label */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="cluster_label">Cluster Label *</Label>
|
|
||||||
<Input
|
|
||||||
id="cluster_label"
|
|
||||||
name="cluster_label"
|
|
||||||
value={formData.cluster_label}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="my-cluster"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Data Center and Version */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="dcslug">Data Center *</Label>
|
|
||||||
<Select
|
|
||||||
name="dcslug"
|
|
||||||
value={formData.dcslug}
|
|
||||||
onValueChange={(value) => setFormData({...formData, dcslug: value})}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select location" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="innoida">Noida</SelectItem>
|
|
||||||
<SelectItem value="inmumbaizone2">Mumbai Zone 2</SelectItem>
|
|
||||||
<SelectItem value="indelhi">Delhi</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="cluster_version">Kubernetes Version *</Label>
|
|
||||||
<Select
|
|
||||||
name="cluster_version"
|
|
||||||
value={formData.cluster_version}
|
|
||||||
onValueChange={(value) => setFormData({...formData, cluster_version: value})}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select version" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="1.24">1.24</SelectItem>
|
|
||||||
<SelectItem value="1.25">1.25</SelectItem>
|
|
||||||
<SelectItem value="1.26">1.26</SelectItem>
|
|
||||||
<SelectItem value="1.27">1.27</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Network Type and CPU Model */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="network_type">Network Type *</Label>
|
|
||||||
<Select
|
|
||||||
name="network_type"
|
|
||||||
value={formData.network_type}
|
|
||||||
onValueChange={(value) => setFormData({...formData, network_type: value})}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select network type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="public">Public</SelectItem>
|
|
||||||
<SelectItem value="private">Private</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="cpumodel">CPU Model *</Label>
|
|
||||||
<Select
|
|
||||||
name="cpumodel"
|
|
||||||
value={formData.cpumodel}
|
|
||||||
onValueChange={(value) => setFormData({...formData, cpumodel: value})}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select CPU model" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="intel">Intel</SelectItem>
|
|
||||||
<SelectItem value="amd">AMD</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Node Pool Configuration */}
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-lg font-medium">Node Pool Configuration</h3>
|
<div className="space-y-2">
|
||||||
{formData.nodepools.map((pool, index) => (
|
<Label htmlFor="cluster_label">Cluster Name *</Label>
|
||||||
<div key={index} className="p-4 border rounded-lg space-y-4">
|
<Input
|
||||||
<div className="space-y-2">
|
id="cluster_label"
|
||||||
<Label htmlFor={`pool-label-${index}`}>Pool Label *</Label>
|
name="cluster_label"
|
||||||
<Input
|
value={formData.cluster_label}
|
||||||
id={`pool-label-${index}`}
|
onChange={handleChange}
|
||||||
value={pool.label}
|
placeholder="my-cluster"
|
||||||
onChange={(e) => handleNodePoolChange(index, 'label', e.target.value)}
|
required
|
||||||
required
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="flex justify-between items-center">
|
||||||
<Label htmlFor={`pool-size-${index}`}>Node Size *</Label>
|
<Label className="text-lg">Node Pools</Label>
|
||||||
<Select
|
<Button
|
||||||
value={pool.size}
|
type="button"
|
||||||
onValueChange={(value) => handleNodePoolChange(index, 'size', value)}
|
onClick={addNodePool}
|
||||||
required
|
variant="outline"
|
||||||
>
|
size="sm"
|
||||||
<SelectTrigger>
|
>
|
||||||
<SelectValue placeholder="Select node size" />
|
Add Node Pool
|
||||||
</SelectTrigger>
|
</Button>
|
||||||
<SelectContent>
|
|
||||||
{plans.map(plan => (
|
|
||||||
<SelectItem key={plan.id} value={plan.id}>
|
|
||||||
{`${plan.cpu} vCPU, ${Math.floor(parseInt(plan.ram)/1024)}GB RAM`}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`pool-count-${index}`}>Node Count *</Label>
|
|
||||||
<Input
|
|
||||||
id={`pool-count-${index}`}
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
value={pool.count}
|
|
||||||
onChange={(e) => handleNodePoolChange(index, 'count', parseInt(e.target.value))}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`pool-maxCount-${index}`}>Max Nodes</Label>
|
|
||||||
<Input
|
|
||||||
id={`pool-maxCount-${index}`}
|
|
||||||
type="number"
|
|
||||||
min={pool.count}
|
|
||||||
value={pool.maxCount}
|
|
||||||
onChange={(e) => handleNodePoolChange(index, 'maxCount', parseInt(e.target.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* VPC and Subnet */}
|
{nodePools.map((pool, poolIndex) => (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div key={poolIndex} className="p-4 border rounded-lg space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="flex justify-between items-center">
|
||||||
<Label htmlFor="vpc">VPC *</Label>
|
<h4 className="font-medium">Node Pool #{poolIndex + 1}</h4>
|
||||||
<Select
|
{nodePools.length > 1 && (
|
||||||
name="vpc"
|
<Button
|
||||||
value={formData.vpc}
|
type="button"
|
||||||
onValueChange={handleVpcChange}
|
variant="destructive"
|
||||||
required
|
size="sm"
|
||||||
>
|
onClick={() => removeNodePool(poolIndex)}
|
||||||
<SelectTrigger>
|
>
|
||||||
<SelectValue placeholder="Select VPC" />
|
Remove
|
||||||
</SelectTrigger>
|
</Button>
|
||||||
<SelectContent>
|
)}
|
||||||
{vpcs.map(vpc => (
|
</div>
|
||||||
<SelectItem key={vpc.id} value={vpc.id}>
|
|
||||||
{vpc.name || vpc.id}
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
</SelectItem>
|
<div className="space-y-2">
|
||||||
))}
|
<Label>Pool Label</Label>
|
||||||
</SelectContent>
|
<Input
|
||||||
</Select>
|
value={pool.label}
|
||||||
|
onChange={(e) => handleNodePoolChange(poolIndex, 'label', e.target.value)}
|
||||||
|
placeholder="worker-pool"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Node Size *</Label>
|
||||||
|
<Select
|
||||||
|
value={pool.size}
|
||||||
|
onValueChange={(value) => handleNodePoolChange(poolIndex, 'size', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select size" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableSizes.map(size => (
|
||||||
|
<SelectItem key={size.id} value={size.id}>
|
||||||
|
{size.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Node Count *</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={pool.count}
|
||||||
|
onChange={(e) => handleNodePoolChange(poolIndex, 'count', parseInt(e.target.value) || 1)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Label className="text-md">Storage Volumes</Label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => addDisk(poolIndex)}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Add Volume
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pool.ebs.map((disk, diskIndex) => (
|
||||||
|
<div key={diskIndex} className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Size (GB) *</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={disk.disk}
|
||||||
|
onChange={(e) => handleDiskChange(poolIndex, diskIndex, 'disk', e.target.value)}
|
||||||
|
placeholder="30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Type *</Label>
|
||||||
|
<Select
|
||||||
|
value={disk.type}
|
||||||
|
onValueChange={(value) => handleDiskChange(poolIndex, diskIndex, 'type', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem selected value="nvme">NVMe SSD</SelectItem>
|
||||||
|
{/* <SelectItem value="ssd">Standard SSD</SelectItem>
|
||||||
|
<SelectItem value="hdd">HDD</SelectItem> */}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
{pool.ebs.length > 1 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeDisk(poolIndex, diskIndex)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="subnet">Subnet *</Label>
|
|
||||||
<Select
|
|
||||||
name="subnet"
|
|
||||||
value={formData.subnet}
|
|
||||||
onValueChange={(value) => setFormData({...formData, subnet: value})}
|
|
||||||
required
|
|
||||||
disabled={!formData.vpc || subnets.length === 0}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder={subnets.length === 0 ? "No subnets available" : "Select subnet"}>
|
|
||||||
{formData.subnet || (subnets.length === 0 ? "No subnets available" : "Select subnet")}
|
|
||||||
</SelectValue>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{subnets.map(subnet => (
|
|
||||||
<SelectItem key={subnet.id} value={subnet.id}>
|
|
||||||
{subnet.name || subnet.id} ({subnet.network}/{subnet.size})
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{subnets.length === 0 && formData.vpc && (
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
No subnets found in this VPC. Please create a subnet first.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Firewall (Optional) */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="firewall">Firewall ID (Optional)</Label>
|
|
||||||
<Input
|
|
||||||
id="firewall"
|
|
||||||
name="firewall"
|
|
||||||
value={formData.firewall}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="Firewall ID"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end pt-4">
|
<div className="flex justify-end pt-4">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading || !formData.cluster_label || !formData.nodepools[0].size || !formData.vpc || !formData.subnet}
|
disabled={isLoading}
|
||||||
className="w-full md:w-auto"
|
className="w-full md:w-auto"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Loader className="mr-2 h-4 w-4" />
|
<Loader size="sm" className="mr-2" />
|
||||||
Deploying...
|
Deploying...
|
||||||
</>
|
</>
|
||||||
) : "Deploy Kubernetes Cluster"}
|
) : (
|
||||||
|
"Deploy Cluster"
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -9,48 +9,294 @@ import { useToast } from "../ui/toast";
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
|
||||||
import { Textarea } from "../ui/textarea";
|
import { Textarea } from "../ui/textarea";
|
||||||
import { Switch } from "../ui/switch";
|
import { Switch } from "../ui/switch";
|
||||||
|
import { useIsLoggedIn } from '../../lib/isLoggedIn';
|
||||||
|
|
||||||
export default function NewCloudInstance() {
|
export default function NewCloudInstance() {
|
||||||
const PUBLIC_UTHO_API_KEY = import.meta.env.PUBLIC_UTHO_API_KEY;
|
const PUBLIC_UTHO_API_KEY = import.meta.env.PUBLIC_UTHO_API_KEY;
|
||||||
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isFetchingData, setIsFetchingData] = useState(true);
|
const [isFetchingData, setIsFetchingData] = useState(true);
|
||||||
const [plans, setPlans] = useState([]);
|
const [deployError, setDeployError] = useState()
|
||||||
const [sshKeys, setSshKeys] = useState([]);
|
const [deployStatus, setDeployStatus] = useState({});
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [plans, setPlans] = useState([
|
||||||
|
{
|
||||||
|
"id": "10027",
|
||||||
|
"type": "cloud",
|
||||||
|
"slug": "dedicated-cpu",
|
||||||
|
"disk": "80",
|
||||||
|
"ram": "4096",
|
||||||
|
"cpu": "2",
|
||||||
|
"bandwidth": "1000",
|
||||||
|
"dedicated_vcore": "1",
|
||||||
|
"is_available": "YES",
|
||||||
|
"price": 26.83,
|
||||||
|
"price_currency": 26.83,
|
||||||
|
"price_cur": "$26.8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "10028",
|
||||||
|
"type": "cloud",
|
||||||
|
"slug": "dedicated-cpu",
|
||||||
|
"disk": "160",
|
||||||
|
"ram": "8192",
|
||||||
|
"cpu": "4",
|
||||||
|
"bandwidth": "2000",
|
||||||
|
"dedicated_vcore": "1",
|
||||||
|
"is_available": "YES",
|
||||||
|
"price": 53.72,
|
||||||
|
"price_currency": 53.72,
|
||||||
|
"price_cur": "$53.7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "10029",
|
||||||
|
"type": "cloud",
|
||||||
|
"slug": "dedicated-cpu",
|
||||||
|
"disk": "480",
|
||||||
|
"ram": "32768",
|
||||||
|
"cpu": "8",
|
||||||
|
"bandwidth": "3000",
|
||||||
|
"dedicated_vcore": "1",
|
||||||
|
"is_available": "YES",
|
||||||
|
"price": 151.91,
|
||||||
|
"price_currency": 151.91,
|
||||||
|
"price_cur": "$151.9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "10030",
|
||||||
|
"type": "cloud",
|
||||||
|
"slug": "dedicated-cpu",
|
||||||
|
"disk": "960",
|
||||||
|
"ram": "65536",
|
||||||
|
"cpu": "16",
|
||||||
|
"bandwidth": "4000",
|
||||||
|
"dedicated_vcore": "1",
|
||||||
|
"is_available": "YES",
|
||||||
|
"price": 292.19,
|
||||||
|
"price_currency": 292.19,
|
||||||
|
"price_cur": "$292.2"
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
// const [sshKeys, setSshKeys] = useState([]);
|
||||||
const [showAddSshKey, setShowAddSshKey] = useState(false);
|
const [showAddSshKey, setShowAddSshKey] = useState(false);
|
||||||
|
const [vpcs, setVpcs] = useState();
|
||||||
|
const [images, setImages] = useState([
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Alma Linux",
|
||||||
|
"version": "9.2 x86_64",
|
||||||
|
"image": "almalinux-9.2-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Alma Linux",
|
||||||
|
"version": "8.4 x86_64",
|
||||||
|
"image": "AlmaLinux_8.4_x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Centos",
|
||||||
|
"version": "7.3 x86_64",
|
||||||
|
"image": "centos-7.3-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Centos",
|
||||||
|
"version": "8 x86_64",
|
||||||
|
"image": "centos-8-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Centos",
|
||||||
|
"version": "7.9 x86_64",
|
||||||
|
"image": "centos-7.9-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Centos",
|
||||||
|
"version": "Ameyo x86_64",
|
||||||
|
"image": "AmeyoCentos7",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Debian",
|
||||||
|
"version": "9.4 x86_64",
|
||||||
|
"image": "debian-9.4-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Debian",
|
||||||
|
"version": "9.13 x86_64",
|
||||||
|
"image": "debian-9.13-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Debian",
|
||||||
|
"version": "10 x86_64",
|
||||||
|
"image": "debian-10-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Debian",
|
||||||
|
"version": "11 x86_64",
|
||||||
|
"image": "debian-11-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Debian",
|
||||||
|
"version": "12 x86_64",
|
||||||
|
"image": "debian-12-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Fedora",
|
||||||
|
"version": "34 x86_64",
|
||||||
|
"image": "fedora-34-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Fedora",
|
||||||
|
"version": "32 x86_64",
|
||||||
|
"image": "fedora-32-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Fedora",
|
||||||
|
"version": "33 x86_64",
|
||||||
|
"image": "fedora-33-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "FortiOS",
|
||||||
|
"version": "7.6.2 ",
|
||||||
|
"image": "fortios-762",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Rocky Linux",
|
||||||
|
"version": "8.7 x86_64",
|
||||||
|
"image": "rocky-8.7-86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Rocky Linux",
|
||||||
|
"version": "8.8 x86_64",
|
||||||
|
"image": "rocky-8.8-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Rocky Linux",
|
||||||
|
"version": "9.1 x86_64",
|
||||||
|
"image": "rocky-9.1-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Rocky Linux",
|
||||||
|
"version": "9.2 x86_64",
|
||||||
|
"image": "rocky-9.2-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Ubuntu",
|
||||||
|
"version": "20.04 x86_64",
|
||||||
|
"image": "ubuntu-20.04-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Ubuntu",
|
||||||
|
"version": "18.04 x86_64",
|
||||||
|
"image": "ubuntu-18.10-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Ubuntu",
|
||||||
|
"version": "22.04 x86_64",
|
||||||
|
"image": "ubuntu-22.04-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Windows",
|
||||||
|
"version": "Server 2012R2 x86_64",
|
||||||
|
"image": "windows-server2012r2-x86_64",
|
||||||
|
"cost": 1200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Windows",
|
||||||
|
"version": "Server 2016 x86_64",
|
||||||
|
"image": "windows-2016",
|
||||||
|
"cost": 1200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Windows",
|
||||||
|
"version": "Server 2019 x86_64",
|
||||||
|
"image": "windows2019-10nov2020",
|
||||||
|
"cost": 1200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Windows",
|
||||||
|
"version": "Server 2022 x86_64",
|
||||||
|
"image": "windows-2022",
|
||||||
|
"cost": 1200
|
||||||
|
}
|
||||||
|
])
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
dcslug: "inmumbaizone2",
|
dcslug: '',
|
||||||
planid: "",
|
planid: '',
|
||||||
hostname: "",
|
billingcycle: "hourly",
|
||||||
image: "debian-12-x86_64",
|
auth: "option2",
|
||||||
auth: "ssh_key",
|
enable_publicip: '',
|
||||||
sshkeys: "",
|
subnetRequired: '',
|
||||||
password: "",
|
firewall: "23434645",
|
||||||
enable_publicip: true,
|
cpumodel: "intel",
|
||||||
enablebackup: false,
|
enablebackup: '',
|
||||||
vpc: "bcb68d3d-60f5-495f-9513-6b6a4e53a470",
|
root_password: '',
|
||||||
billingcycle: "hourly"
|
support: "unmanaged",
|
||||||
|
vpc: '',
|
||||||
|
cloud: [
|
||||||
|
{
|
||||||
|
hostname: ''
|
||||||
|
}
|
||||||
|
],
|
||||||
|
image: '',
|
||||||
|
sshkeys: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
// Fetch plans and SSH keys in parallel
|
const vpcResponse = await fetch('https://api.utho.com/v2/vpc', {
|
||||||
const [plansResponse, sshResponse] = await Promise.all([
|
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
|
||||||
fetch("https://api.utho.com/v2/plans", {
|
});
|
||||||
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
|
|
||||||
}),
|
|
||||||
fetch("https://api.utho.com/v2/key", {
|
|
||||||
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
|
|
||||||
const plansData = await plansResponse.json();
|
const vpcsData = await vpcResponse.json();
|
||||||
const sshData = await sshResponse.json();
|
|
||||||
|
|
||||||
setPlans(plansData.plans || []);
|
setVpcs(vpcsData.vpc || []);
|
||||||
setSshKeys(sshData.key || []);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast({
|
showToast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
|
@ -65,6 +311,7 @@ export default function NewCloudInstance() {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
@ -73,26 +320,37 @@ export default function NewCloudInstance() {
|
||||||
const payload = {
|
const payload = {
|
||||||
dcslug: formData.dcslug,
|
dcslug: formData.dcslug,
|
||||||
planid: formData.planid,
|
planid: formData.planid,
|
||||||
hostname: formData.hostname,
|
billingcycle: "hourly",
|
||||||
|
auth: "option2",
|
||||||
|
enable_publicip: formData.enable_publicip ? true : false,
|
||||||
|
subnetRequired: false,
|
||||||
|
firewall: "23434645",
|
||||||
|
cpumodel: "intel",
|
||||||
|
enablebackup: formData.enablebackup ? true : false,
|
||||||
|
root_password: formData.rootPassword,
|
||||||
|
support: "unmanaged",
|
||||||
|
vpc: formData.dcslug === 'inmumbaizone2' ? '68c36d65-bda8-43cc-94b5-ae28bb2c3306' : formData.dcslug === 'inbangalore' ? 'c17032c9-3cfd-4028-8f2a-f3f5aa8c2976' : '',
|
||||||
|
cloud: [
|
||||||
|
{
|
||||||
|
hostname: formData.hostname
|
||||||
|
}
|
||||||
|
],
|
||||||
image: formData.image,
|
image: formData.image,
|
||||||
enable_publicip: formData.enable_publicip ? "true" : "false",
|
sshkeys: formData.sshkeys
|
||||||
enablebackup: formData.enablebackup ? "true" : "false",
|
};
|
||||||
cloud: [{ hostname: formData.hostname }],
|
// inbangalore = c17032c9-3cfd-4028-8f2a-f3f5aa8c2976
|
||||||
vpc: "bcb68d3d-60f5-495f-9513-6b6a4e53a470",
|
// inmumbai = 68c36d65-bda8-43cc-94b5-ae28bb2c3306
|
||||||
billingcycle: "hourly"
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add authentication based on selected method
|
// Add authentication based on selected method
|
||||||
if (formData.auth === "ssh_key") {
|
if (formData.auth === "ssh_key") {
|
||||||
payload.sshkeys = formData.sshkeys;
|
payload.sshkeys = formData.sshkeys;
|
||||||
} else {
|
} else {
|
||||||
payload.password = formData.password;
|
payload.password = formData.password;
|
||||||
}
|
}
|
||||||
|
console.log('payload', payload)
|
||||||
const response = await fetch("https://api.utho.com/v2/cloud/deploy", {
|
const response = await fetch("https://host-api.cs1.hz.siliconpin.com/v1/vps/?query=deploy&source=chanel_1", {
|
||||||
|
credentials: "include",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
|
@ -101,11 +359,12 @@ export default function NewCloudInstance() {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.status === "success") {
|
if (data.status === "success") {
|
||||||
showToast({
|
setDeployStatus({status : data.status, ip : data.ipv4})
|
||||||
title: "Deployment Started",
|
// showToast({
|
||||||
description: `Server ${data.cloudid} is being deployed with IP ${data.ipv4}`,
|
// title: "Deployment Started",
|
||||||
variant: "success"
|
// description: `Server ${data.cloudid} is being deployed with IP ${data.ipv4}`,
|
||||||
});
|
// variant: "success"
|
||||||
|
// });
|
||||||
// Reset form after successful deployment
|
// Reset form after successful deployment
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
@ -116,11 +375,7 @@ export default function NewCloudInstance() {
|
||||||
throw new Error(data.message || "Failed to deploy instance");
|
throw new Error(data.message || "Failed to deploy instance");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast({
|
setDeployError(error.message)
|
||||||
title: "Deployment Failed",
|
|
||||||
description: error.message,
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -175,17 +430,60 @@ export default function NewCloudInstance() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isFetchingData) {
|
const handleCopy = (text) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1000); // revert after 1 second
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (loading || (isLoggedIn && isFetchingData)) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (deployError) return <p>Error: {deployError?.message}</p>;
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
return <p className="text-center mt-8">
|
||||||
|
You are not Logged in. <a href="/login" className="text-[#6d9e37]">Click Here</a> to login first then you can add this service.
|
||||||
|
</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if(deployStatus.status === 'success'){
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card className="w-full max-w-2xl mx-auto my-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Create New Vertual Private Server (VPS)</CardTitle>
|
||||||
|
<CardDescription>Provide the root password then you can access vps using the password. <br /> later on you can setup ssh key</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p>Your New VPS</p>
|
||||||
|
<div className="flex flex-col my-2">
|
||||||
|
<Label>IP Address:</Label>
|
||||||
|
<div className="flex">
|
||||||
|
<Input className="rounded-l-md rounded-r-none text-xl font-bold border-[2px] border-[#6d9e37]" type="text" readOnly value={deployStatus.ip} />
|
||||||
|
<Button className="rounded-l-none rounded-r-md" onClick={() => handleCopy(deployStatus.ip)}>{copied ? 'Copied!' : 'Copy' }</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center place-items-center">
|
||||||
|
<Button className="" onClick={() => window.location.href = '/services/cloud-instance'}>Back</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-2xl mx-auto my-4">
|
<Card className="w-full max-w-2xl mx-auto my-4">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Deploy New Cloud Instance</CardTitle>
|
<CardTitle>Create New Vertual Private Server (VPS)</CardTitle>
|
||||||
<CardDescription>Configure your cloud server with essential parameters</CardDescription>
|
<CardDescription>Provide the root password then you can access vps using the password. <br /> later on you can setup ssh key</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="hostname">Hostname *</Label>
|
<Label htmlFor="hostname">Hostname *</Label>
|
||||||
|
@ -211,9 +509,8 @@ export default function NewCloudInstance() {
|
||||||
<SelectValue placeholder="Select location" />
|
<SelectValue placeholder="Select location" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="inmumbaizone2">Mumbai Zone 2</SelectItem>
|
<SelectItem value="inmumbaizone2">Mumbai</SelectItem>
|
||||||
<SelectItem value="innoida">Noida</SelectItem>
|
<SelectItem value="inbangalore">Bangalore</SelectItem>
|
||||||
<SelectItem value="indelhi">Delhi</SelectItem>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
@ -230,38 +527,53 @@ export default function NewCloudInstance() {
|
||||||
<SelectValue placeholder="Select a plan" />
|
<SelectValue placeholder="Select a plan" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{/* {plans.map(plan => (
|
{plans.map(plan => (
|
||||||
<SelectItem key={plan.id} value={plan.id}>
|
<SelectItem key={plan.id} value={plan.id}>
|
||||||
{`${plan.cpu} vCPU, ${plan.ram}MB RAM - ${plan.price_cur}/mo`}
|
{`${plan.cpu} vCPU, ${plan.ram}MB RAM - ${plan.price_cur}/mo`}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))} */}
|
))}
|
||||||
<SelectItem key={plans[0].id} value={plans[0].id}>
|
{/* <SelectItem key={plans[0].id} value={plans[0].id}>
|
||||||
{`${plans[0].cpu} vCPU, ${plans[0].ram}MB RAM - ${plans[0].price_cur}/mo`}
|
{`${plans[0].cpu} vCPU, ${plans[0].ram}MB RAM - ${plans[0].price_cur}/mo`}
|
||||||
</SelectItem>
|
</SelectItem> */}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="image">Operating System *</Label>
|
||||||
|
<Select
|
||||||
|
name="image"
|
||||||
|
value={formData.image}
|
||||||
|
onValueChange={(value) => setFormData({...formData, image: value})}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select OS" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{
|
||||||
|
images.map((image, i) => (
|
||||||
|
<SelectItem key={i} value={image.image}>{image.image.split('-x86_64')[0]}</SelectItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="image">Operating System *</Label>
|
<Label htmlFor="password">Root Password *</Label>
|
||||||
<Select
|
<Input
|
||||||
name="image"
|
id="password"
|
||||||
value={formData.image}
|
name="password"
|
||||||
onValueChange={(value) => setFormData({...formData, image: value})}
|
type="password"
|
||||||
>
|
value={formData.password}
|
||||||
<SelectTrigger>
|
onChange={handleChange}
|
||||||
<SelectValue placeholder="Select OS" />
|
placeholder="Enter root password"
|
||||||
</SelectTrigger>
|
required={formData.auth === "password"}
|
||||||
<SelectContent>
|
/>
|
||||||
<SelectItem value="debian-12-x86_64">Debian 12</SelectItem>
|
</div>
|
||||||
<SelectItem value="ubuntu-22.04">Ubuntu 22.04</SelectItem>
|
|
||||||
<SelectItem value="centos-7.4-x86_64">CentOS 7.4</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
{/* <div className="space-y-4">
|
||||||
<h3 className="text-lg font-medium">Authentication</h3>
|
<h3 className="text-lg font-medium">Authentication</h3>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={formData.auth}
|
value={formData.auth}
|
||||||
|
@ -349,7 +661,7 @@ export default function NewCloudInstance() {
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-lg font-medium">Options</h3>
|
<h3 className="text-lg font-medium">Options</h3>
|
||||||
|
|
|
@ -0,0 +1,615 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
||||||
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import { Label } from "../ui/label";
|
||||||
|
import Loader from "../ui/loader";
|
||||||
|
import { useToast } from "../ui/toast";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
|
||||||
|
import { Textarea } from "../ui/textarea";
|
||||||
|
import { Switch } from "../ui/switch";
|
||||||
|
|
||||||
|
export default function NewCloudInstance() {
|
||||||
|
const PUBLIC_UTHO_API_KEY = import.meta.env.PUBLIC_UTHO_API_KEY;
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isFetchingData, setIsFetchingData] = useState(true);
|
||||||
|
const [plans, setPlans] = useState([]);
|
||||||
|
const [sshKeys, setSshKeys] = useState([]);
|
||||||
|
const [showAddSshKey, setShowAddSshKey] = useState(false);
|
||||||
|
const [vpcs, setVpcs] = useState();
|
||||||
|
const [images, setImages] = useState([
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Alma Linux",
|
||||||
|
"version": "9.2 x86_64",
|
||||||
|
"image": "almalinux-9.2-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Alma Linux",
|
||||||
|
"version": "8.4 x86_64",
|
||||||
|
"image": "AlmaLinux_8.4_x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Centos",
|
||||||
|
"version": "7.3 x86_64",
|
||||||
|
"image": "centos-7.3-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Centos",
|
||||||
|
"version": "8 x86_64",
|
||||||
|
"image": "centos-8-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Centos",
|
||||||
|
"version": "7.9 x86_64",
|
||||||
|
"image": "centos-7.9-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Centos",
|
||||||
|
"version": "Ameyo x86_64",
|
||||||
|
"image": "AmeyoCentos7",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Debian",
|
||||||
|
"version": "9.4 x86_64",
|
||||||
|
"image": "debian-9.4-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Debian",
|
||||||
|
"version": "9.13 x86_64",
|
||||||
|
"image": "debian-9.13-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Debian",
|
||||||
|
"version": "10 x86_64",
|
||||||
|
"image": "debian-10-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Debian",
|
||||||
|
"version": "11 x86_64",
|
||||||
|
"image": "debian-11-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Debian",
|
||||||
|
"version": "12 x86_64",
|
||||||
|
"image": "debian-12-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Fedora",
|
||||||
|
"version": "34 x86_64",
|
||||||
|
"image": "fedora-34-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Fedora",
|
||||||
|
"version": "32 x86_64",
|
||||||
|
"image": "fedora-32-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Fedora",
|
||||||
|
"version": "33 x86_64",
|
||||||
|
"image": "fedora-33-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "FortiOS",
|
||||||
|
"version": "7.6.2 ",
|
||||||
|
"image": "fortios-762",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Rocky Linux",
|
||||||
|
"version": "8.7 x86_64",
|
||||||
|
"image": "rocky-8.7-86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Rocky Linux",
|
||||||
|
"version": "8.8 x86_64",
|
||||||
|
"image": "rocky-8.8-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Rocky Linux",
|
||||||
|
"version": "9.1 x86_64",
|
||||||
|
"image": "rocky-9.1-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Rocky Linux",
|
||||||
|
"version": "9.2 x86_64",
|
||||||
|
"image": "rocky-9.2-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Ubuntu",
|
||||||
|
"version": "20.04 x86_64",
|
||||||
|
"image": "ubuntu-20.04-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Ubuntu",
|
||||||
|
"version": "18.04 x86_64",
|
||||||
|
"image": "ubuntu-18.10-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Ubuntu",
|
||||||
|
"version": "22.04 x86_64",
|
||||||
|
"image": "ubuntu-22.04-x86_64",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Windows",
|
||||||
|
"version": "Server 2012R2 x86_64",
|
||||||
|
"image": "windows-server2012r2-x86_64",
|
||||||
|
"cost": 1200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Windows",
|
||||||
|
"version": "Server 2016 x86_64",
|
||||||
|
"image": "windows-2016",
|
||||||
|
"cost": 1200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Windows",
|
||||||
|
"version": "Server 2019 x86_64",
|
||||||
|
"image": "windows2019-10nov2020",
|
||||||
|
"cost": 1200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"distro": null,
|
||||||
|
"distribution": "Windows",
|
||||||
|
"version": "Server 2022 x86_64",
|
||||||
|
"image": "windows-2022",
|
||||||
|
"cost": 1200
|
||||||
|
}
|
||||||
|
])
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
dcslug: '',
|
||||||
|
planid: '',
|
||||||
|
billingcycle: "hourly",
|
||||||
|
auth: "option2",
|
||||||
|
enable_publicip: '',
|
||||||
|
subnetRequired: '',
|
||||||
|
firewall: "23434645",
|
||||||
|
cpumodel: "intel",
|
||||||
|
enablebackup: '',
|
||||||
|
root_password: '',
|
||||||
|
support: "unmanaged",
|
||||||
|
vpc: '',
|
||||||
|
cloud: [
|
||||||
|
{
|
||||||
|
hostname: ''
|
||||||
|
}
|
||||||
|
],
|
||||||
|
image: '',
|
||||||
|
sshkeys: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
// Fetch plans and SSH keys in parallel
|
||||||
|
const [plansResponse, sshResponse, vpcLists] = await Promise.all([
|
||||||
|
fetch("https://api.utho.com/v2/plans", {
|
||||||
|
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
|
||||||
|
}),
|
||||||
|
fetch("https://api.utho.com/v2/key", {
|
||||||
|
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
|
||||||
|
}),
|
||||||
|
fetch('https://api.utho.com/v2/vpc', {
|
||||||
|
headers: {"Authorization" : `Bearer ${PUBLIC_UTHO_API_KEY}`}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const plansData = await plansResponse.json();
|
||||||
|
const sshData = await sshResponse.json();
|
||||||
|
const vpcsData = await vpcLists.json();
|
||||||
|
|
||||||
|
setPlans(plansData.plans || []);
|
||||||
|
setSshKeys(sshData.key || []);
|
||||||
|
setVpcs(vpcsData.vpc || []);
|
||||||
|
} catch (error) {
|
||||||
|
showToast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load configuration data",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsFetchingData(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
dcslug: formData.dcslug,
|
||||||
|
planid: formData.planid,
|
||||||
|
billingcycle: "hourly",
|
||||||
|
auth: "option2",
|
||||||
|
enable_publicip: formData.enable_publicip ? true : false,
|
||||||
|
subnetRequired: false,
|
||||||
|
firewall: "23434645",
|
||||||
|
cpumodel: "intel",
|
||||||
|
enablebackup: formData.enablebackup ? true : false,
|
||||||
|
root_password: formData.rootPassword,
|
||||||
|
support: "unmanaged",
|
||||||
|
vpc: 'c17032c9-3cfd-4028-8f2a-f3f5aa8c2976',
|
||||||
|
cloud: [
|
||||||
|
{
|
||||||
|
hostname: formData.hostname
|
||||||
|
}
|
||||||
|
],
|
||||||
|
image: formData.image,
|
||||||
|
sshkeys: formData.sshkeys
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add authentication based on selected method
|
||||||
|
if (formData.auth === "ssh_key") {
|
||||||
|
payload.sshkeys = formData.sshkeys;
|
||||||
|
} else {
|
||||||
|
payload.password = formData.password;
|
||||||
|
}
|
||||||
|
console.log('payload', payload)
|
||||||
|
const response = await fetch("https://host-api.cs1.hz.siliconpin.com/v1/vps/?query=deploy&source=chanel_1", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === "success") {
|
||||||
|
showToast({
|
||||||
|
title: "Deployment Started",
|
||||||
|
description: `Server ${data.cloudid} is being deployed with IP ${data.ipv4}`,
|
||||||
|
variant: "success"
|
||||||
|
});
|
||||||
|
// Reset form after successful deployment
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
hostname: "",
|
||||||
|
planid: ""
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || "Failed to deploy instance");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast({
|
||||||
|
title: "Deployment Failed",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value, type, checked } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: type === "checkbox" ? checked : value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddSshKey = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.utho.com/v2/key/import', {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: formData.newSshKeyName,
|
||||||
|
sshkey: formData.newSshKeyValue
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// Refresh SSH keys list
|
||||||
|
const sshResponse = await fetch("https://api.utho.com/v2/key", {
|
||||||
|
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
|
||||||
|
});
|
||||||
|
const sshData = await sshResponse.json();
|
||||||
|
setSshKeys(sshData.key || []);
|
||||||
|
|
||||||
|
showToast({
|
||||||
|
title: "SSH Key Added",
|
||||||
|
description: "Your SSH key has been successfully added",
|
||||||
|
variant: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
setShowAddSshKey(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to add SSH key",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isFetchingData) {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-2xl mx-auto my-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Deploy New Cloud Instance</CardTitle>
|
||||||
|
<CardDescription>Configure your cloud server with essential parameters</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="hostname">Hostname *</Label>
|
||||||
|
<Input
|
||||||
|
id="hostname"
|
||||||
|
name="hostname"
|
||||||
|
value={formData.hostname}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="server1.example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="dcslug">Data Center *</Label>
|
||||||
|
<Select
|
||||||
|
name="dcslug"
|
||||||
|
value={formData.dcslug}
|
||||||
|
onValueChange={(value) => setFormData({...formData, dcslug: value})}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select location" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="inmumbaizone2">Mumbai</SelectItem>
|
||||||
|
<SelectItem value="inbangalore">Bangalore</SelectItem>
|
||||||
|
<SelectItem value="innoida">Delhi (Noida)</SelectItem>
|
||||||
|
<SelectItem value="defra1">Germany</SelectItem>
|
||||||
|
<SelectItem value="uslosangeles">Los Angeles</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="planid">Plan *</Label>
|
||||||
|
<Select
|
||||||
|
name="planid"
|
||||||
|
value={formData.planid}
|
||||||
|
onValueChange={(value) => setFormData({...formData, planid: value})}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a plan" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{plans.map(plan => (
|
||||||
|
<SelectItem key={plan.id} value={plan.id}>
|
||||||
|
{`${plan.cpu} vCPU, ${plan.ram}MB RAM - ${plan.price_cur}/mo`}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
{/* <SelectItem key={plans[0].id} value={plans[0].id}>
|
||||||
|
{`${plans[0].cpu} vCPU, ${plans[0].ram}MB RAM - ${plans[0].price_cur}/mo`}
|
||||||
|
</SelectItem> */}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="image">Operating System *</Label>
|
||||||
|
<Select
|
||||||
|
name="image"
|
||||||
|
value={formData.image}
|
||||||
|
onValueChange={(value) => setFormData({...formData, image: value})}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select OS" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{
|
||||||
|
images.map((image, i) => (
|
||||||
|
<SelectItem key={i} value={image.image}>{image.image.split('-x86_64')[0]}</SelectItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="vpc">VPC *</Label>
|
||||||
|
<Select
|
||||||
|
name="vpc"
|
||||||
|
value={formData.vpc}
|
||||||
|
onValueChange={(value) => setFormData({...formData, vpc: value})}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select VPC" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{
|
||||||
|
vpcs.map((vpc, n) => (
|
||||||
|
<SelectItem key={n} value={`${vpc.id}`}>{vpc.dclocation.location}</SelectItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Authentication</h3>
|
||||||
|
<Tabs
|
||||||
|
value={formData.auth}
|
||||||
|
onValueChange={(value) => setFormData({...formData, auth: value})}
|
||||||
|
className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-2 gap-4">
|
||||||
|
<TabsTrigger className={`p-2 rounded-md ${formData.auth === 'ssh_key' ? 'bg-[#6d9e37] text-[#FFF]' : 'border border-[#6d9e37]'}`} value="ssh_key">SSH Key</TabsTrigger>
|
||||||
|
<TabsTrigger className={`p-2 rounded-md ${formData.auth === 'password' ? 'bg-[#6d9e37] text-[#FFF]' : 'border border-[#6d9e37]'}`} value="password">Password</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="ssh_key" className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Select SSH Key</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select
|
||||||
|
value={formData.sshkeys}
|
||||||
|
onValueChange={(value) => setFormData({...formData, sshkeys: value})}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select SSH key" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{sshKeys.map(key => (
|
||||||
|
<SelectItem key={key.id} value={key.id}>{key.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
onClick={() => setShowAddSshKey(!showAddSshKey)}
|
||||||
|
>
|
||||||
|
{showAddSshKey ? "Cancel" : "Add New"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showAddSshKey && (
|
||||||
|
<div className="space-y-4 p-4 border rounded-lg">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="newSshKeyName">Key Name</Label>
|
||||||
|
<Input
|
||||||
|
id="newSshKeyName"
|
||||||
|
name="newSshKeyName"
|
||||||
|
value={formData.newSshKeyName}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="My Laptop Key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="newSshKeyValue">Public Key</Label>
|
||||||
|
<Textarea
|
||||||
|
id="newSshKeyValue"
|
||||||
|
name="newSshKeyValue"
|
||||||
|
value={formData.newSshKeyValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="ssh-rsa AAAAB3NzaC1yc2E..."
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddSshKey}
|
||||||
|
disabled={!formData.newSshKeyName || !formData.newSshKeyValue}
|
||||||
|
>
|
||||||
|
Add SSH Key
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="password" className="space-y-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password">Root Password *</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Enter root password"
|
||||||
|
required={formData.auth === "password"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Options</h3>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
id="enable_publicip"
|
||||||
|
checked={formData.enable_publicip}
|
||||||
|
onCheckedChange={(checked) => setFormData({...formData, enable_publicip: checked})}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="enable_publicip">Enable Public IP</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
id="enablebackup"
|
||||||
|
checked={formData.enablebackup}
|
||||||
|
onCheckedChange={(checked) => setFormData({...formData, enablebackup: checked})}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="enablebackup">Enable Weekly Backups (+20%)</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-4">
|
||||||
|
<Button type="submit" disabled={isLoading} className="w-full md:w-auto">
|
||||||
|
{isLoading ? "Deploying..." : "Deploy Instance"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
const payload = {
|
||||||
|
"dcslug": "inbangalore",
|
||||||
|
"planid": "10045",
|
||||||
|
"billingcycle": "hourly",
|
||||||
|
"auth": "option2",
|
||||||
|
"enable_publicip": "true",
|
||||||
|
"subnetRequired": "false",
|
||||||
|
"firewall": "23434645",
|
||||||
|
"cpumodel": "intel",
|
||||||
|
"enablebackup": "false",
|
||||||
|
"root_password": "5qT381g@K@pDsq",
|
||||||
|
"support": "unmanaged",
|
||||||
|
"vpc": "c17032c9-3cfd-4028-8f2a-f3f5aa8c2976",
|
||||||
|
"cloud": [
|
||||||
|
{
|
||||||
|
"hostname": "cloudserver-ep0yNFPI.mhc"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"image": "debian-9.4-x86_64",
|
||||||
|
"sshkeys": "74131237"
|
||||||
|
};
|
||||||
|
|
||||||
|
// const payload = {
|
||||||
|
// dcslug: formData.dcslug,
|
||||||
|
// planid: formData.planid,
|
||||||
|
// billingcycle: "hourly",
|
||||||
|
// auth: "option2",
|
||||||
|
// enable_publicip: formData.enable_publicip ? true : false,
|
||||||
|
// subnetRequired: false,
|
||||||
|
// firewall: "23434645",
|
||||||
|
// cpumodel: "intel",
|
||||||
|
// enablebackup: formData.enablebackup ? true : false,
|
||||||
|
// root_password: formData.rootPassword,
|
||||||
|
// support: "unmanaged",
|
||||||
|
// vpc: formData.vpc,
|
||||||
|
// cloud: [
|
||||||
|
// {
|
||||||
|
// hostname: formData.hostname
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// image: "debian-9.4-x86_64",
|
||||||
|
// sshkeys: formData.sshkeys
|
||||||
|
// };
|
|
@ -54,7 +54,7 @@ const CommentSystem = ({ topicId, pbId }) => {
|
||||||
params.append('pb_id', pb_id);
|
params.append('pb_id', pb_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`/api/comments?${params.toString()}`);
|
const response = await fetch(`https://sp-comments-prod-pocndhvgmcnbacgb.siliconpin.com/comments?${params.toString()}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
@ -89,9 +89,9 @@ const CommentSystem = ({ topicId, pbId }) => {
|
||||||
payload.parent_comment_id = parentId;
|
payload.parent_comment_id = parentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = parentId ? '/api/comments/reply' : '/api/comments';
|
const endpoint = parentId ? '/comments/reply' : '/comments';
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(`https://sp-comments-prod-pocndhvgmcnbacgb.siliconpin.com${endpoint}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
|
|
|
@ -104,7 +104,7 @@ const LoginPage = () => {
|
||||||
setStatus({ message: '', isError: false });
|
setStatus({ message: '', isError: false });
|
||||||
|
|
||||||
const authData = await pb.collection('users').authWithOAuth2({ provider });
|
const authData = await pb.collection('users').authWithOAuth2({ provider });
|
||||||
|
console.log('Authdata', authData);
|
||||||
if (!authData?.record) {
|
if (!authData?.record) {
|
||||||
throw new Error("No user record found");
|
throw new Error("No user record found");
|
||||||
}
|
}
|
||||||
|
@ -344,6 +344,14 @@ const LoginPage = () => {
|
||||||
>
|
>
|
||||||
<img src="/assets/github.svg" alt="GitHub" className="h-6 w-6" />
|
<img src="/assets/github.svg" alt="GitHub" className="h-6 w-6" />
|
||||||
</Button>
|
</Button>
|
||||||
|
{/* <Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => loginWithOAuth2('gitea')}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<img src="/assets/gitea.svg" alt="GitHub" className="h-6 w-6" />
|
||||||
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,12 @@ import { Button } from './ui/button';
|
||||||
import { Separator } from './ui/separator';
|
import { Separator } from './ui/separator';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
|
||||||
import { CustomTabs } from './ui/tabs';
|
import { CustomTabs } from './ui/tabs';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
|
const PUBLIC_MINIO_UPLOAD_URL = 'https://file-vault-prod-pdgctwgsnkiy.siliconpin.com/upload';
|
||||||
|
const IMAGE_URL_PREFIX = 'https://file-delivery.ndgctwgsnkiy.siliconpin.com/';
|
||||||
|
// const PUBLIC_MINIO_UPLOAD_URL = import.meta.env.PUBLIC_MINIO_UPLOAD_URL;
|
||||||
|
|
||||||
const PUBLIC_MINIO_UPLOAD_URL = import.meta.env.PUBLIC_MINIO_UPLOAD_URL;
|
|
||||||
const PUBLIC_TOPIC_API_URL = import.meta.env.PUBLIC_TOPIC_API_URL;
|
const PUBLIC_TOPIC_API_URL = import.meta.env.PUBLIC_TOPIC_API_URL;
|
||||||
|
|
||||||
const NewTopic = () => {
|
const NewTopic = () => {
|
||||||
|
@ -27,26 +31,30 @@ const NewTopic = () => {
|
||||||
const [imageUploadPreview, setImageUploadPreview] = useState('');
|
const [imageUploadPreview, setImageUploadPreview] = useState('');
|
||||||
const [editorMode, setEditorMode] = useState('edit');
|
const [editorMode, setEditorMode] = useState('edit');
|
||||||
|
|
||||||
// Upload file to MinIO
|
|
||||||
const uploadToMinIO = async (file, onProgress) => {
|
const uploadToMinIO = async (file, onProgress) => {
|
||||||
|
const siliconId = Cookies.get('siliconId'); // Get from cookie or anywhere
|
||||||
|
const token = Cookies.get('token'); // Get from cookie or storage
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('api_key', 'wweifwehfwfhwhtuyegbvijvbfvegfreyf');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(PUBLIC_MINIO_UPLOAD_URL, {
|
const response = await fetch(PUBLIC_MINIO_UPLOAD_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
headers: {
|
||||||
credentials: 'include'
|
'x-user-data': siliconId,
|
||||||
|
'Authorization': token,
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Upload failed');
|
throw new Error(`Upload failed: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log(data.publicUrl)
|
// console.log('Upload successful:', data);
|
||||||
return data.publicUrl;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -78,13 +86,47 @@ const NewTopic = () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get all default commands and replace the image command
|
// Custom <br> insert command
|
||||||
const allCommands = commands.getCommands().map(cmd => {
|
const lineBreakCommand = {
|
||||||
if (cmd.name === 'image') {
|
name: 'linebreak',
|
||||||
return customImageCommand;
|
keyCommand: 'linebreak',
|
||||||
|
buttonProps: { 'aria-label': 'Insert line break' },
|
||||||
|
icon: (
|
||||||
|
<svg width="16px" height="16px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M20 7V8.2C20 9.88016 20 10.7202 19.673 11.362C19.3854 11.9265 18.9265 12.3854 18.362 12.673C17.7202 13 16.8802 13 15.2 13H4M4 13L8 9M4 13L8 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>
|
||||||
|
),
|
||||||
|
execute: () => {
|
||||||
|
const textarea = document.querySelector('.w-md-editor-text-input');
|
||||||
|
if (!textarea) return;
|
||||||
|
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
|
||||||
|
const value = formData.content || "";
|
||||||
|
const breakTag = '<br>';
|
||||||
|
|
||||||
|
const newValue = value.slice(0, start) + breakTag + value.slice(end);
|
||||||
|
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
content: newValue
|
||||||
|
}));
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.selectionStart = textarea.selectionEnd = start + breakTag.length;
|
||||||
|
textarea.focus();
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
return cmd;
|
};
|
||||||
});
|
|
||||||
|
// Get all commands (same as NewTopic)
|
||||||
|
const allCommands = [
|
||||||
|
...commands.getCommands().map(cmd => {
|
||||||
|
if (cmd.name === 'image') {
|
||||||
|
return customImageCommand;
|
||||||
|
}
|
||||||
|
return cmd;
|
||||||
|
}),
|
||||||
|
lineBreakCommand,
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
// Handle image URL insertion
|
// Handle image URL insertion
|
||||||
|
@ -137,7 +179,7 @@ const NewTopic = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Insert markdown for the uploaded image
|
// Insert markdown for the uploaded image
|
||||||
const imgMarkdown = ``;
|
const imgMarkdown = ``;
|
||||||
const textarea = document.querySelector('.w-md-editor-text-input');
|
const textarea = document.querySelector('.w-md-editor-text-input');
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
const startPos = textarea.selectionStart;
|
const startPos = textarea.selectionStart;
|
||||||
|
@ -171,14 +213,14 @@ const NewTopic = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Upload featured image if selected
|
// Upload featured image if selected
|
||||||
let imageUrl = formData.imageUrl;
|
let imageUrl = formData.imageUrl.url;
|
||||||
if (imageFile) {
|
if (imageFile) {
|
||||||
imageUrl = await uploadToMinIO(imageFile, (progress) => {
|
imageUrl = await uploadToMinIO(imageFile, (progress) => {
|
||||||
console.log('imageUrl', imageUrl)
|
console.log('imageUrl', imageUrl)
|
||||||
setUploadProgress(progress);
|
setUploadProgress(progress);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// let finalImageUrl = imageUrl.publicUrl;
|
imageUrl = imageUrl.url;
|
||||||
// Prepare payload
|
// Prepare payload
|
||||||
const payload = {
|
const payload = {
|
||||||
...formData,
|
...formData,
|
||||||
|
@ -296,6 +338,7 @@ const NewTopic = () => {
|
||||||
<SelectValue placeholder="Select a category" />
|
<SelectValue placeholder="Select a category" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent position="popper" className="z-50">
|
<SelectContent position="popper" className="z-50">
|
||||||
|
<SelectItem value="uncategorized">Uncategorized</SelectItem>
|
||||||
<SelectItem value="php">PHP Hosting</SelectItem>
|
<SelectItem value="php">PHP Hosting</SelectItem>
|
||||||
<SelectItem value="nodejs">Node.js Hosting</SelectItem>
|
<SelectItem value="nodejs">Node.js Hosting</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const ImageUploadTest = () => {
|
||||||
|
const [file, setFile] = useState(null);
|
||||||
|
const [preview, setPreview] = useState('');
|
||||||
|
const [status, setStatus] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
const selectedFile = e.target.files[0];
|
||||||
|
|
||||||
|
if (!selectedFile) return;
|
||||||
|
|
||||||
|
// Check if file is an image
|
||||||
|
if (!selectedFile.type.match('image.*')) {
|
||||||
|
setStatus('Error: Please select an image file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFile(selectedFile);
|
||||||
|
setStatus('');
|
||||||
|
|
||||||
|
// Create preview
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => setPreview(reader.result);
|
||||||
|
reader.readAsDataURL(selectedFile);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
setStatus('Error: No file selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setStatus('Uploading...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/check/', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Upload failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.text();
|
||||||
|
setStatus(`Success: ${result}`);
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(`Error: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: '500px', margin: '20px auto', padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
|
||||||
|
<h2>Image Upload Test</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} method='POST' action={'https://file-vault.ndgctwgsnkiy.siliconpin.com/upload'} encType="multipart/form-data">
|
||||||
|
<div style={{ margin: '20px 0' }}>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
id="file-upload"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label htmlFor="file-upload" style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '10px 15px',
|
||||||
|
backgroundColor: '#f0f0f0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: '1px solid #ccc'
|
||||||
|
}}>
|
||||||
|
Choose Image
|
||||||
|
</label>
|
||||||
|
{file && (
|
||||||
|
<span style={{ marginLeft: '10px' }}>{file.name}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{preview && (
|
||||||
|
<div style={{ margin: '20px 0' }}>
|
||||||
|
<img
|
||||||
|
src={preview}
|
||||||
|
alt="Preview"
|
||||||
|
style={{ maxWidth: '100%', maxHeight: '200px', borderRadius: '4px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!file || isLoading}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
backgroundColor: '#4CAF50',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
opacity: (!file || isLoading) ? 0.6 : 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Uploading...' : 'Upload Image'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{status && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: '20px',
|
||||||
|
padding: '10px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: status.startsWith('Error') ? '#ffebee' : '#e8f5e9',
|
||||||
|
color: status.startsWith('Error') ? '#c62828' : '#2e7d32'
|
||||||
|
}}>
|
||||||
|
{status}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ marginTop: '20px', fontSize: '0.9em', color: '#666' }}>
|
||||||
|
<p>Note: This component tests uploading to:</p>
|
||||||
|
<code>https://file-vault.ndgctwgsnkiy.siliconpin.com/upload</code>
|
||||||
|
<p>Make sure:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Your Go server is running</li>
|
||||||
|
<li>CORS is properly configured</li>
|
||||||
|
<li>You have the <code>siliconId</code> cookie set</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImageUploadTest;
|
|
@ -0,0 +1,445 @@
|
||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
|
||||||
|
const FreeTextToSpeech = () => {
|
||||||
|
// State management
|
||||||
|
const [text, setText] = useState('');
|
||||||
|
const [voices, setVoices] = useState([]);
|
||||||
|
const [selectedVoice, setSelectedVoice] = useState('');
|
||||||
|
const [pitch, setPitch] = useState(1);
|
||||||
|
const [rate, setRate] = useState(1);
|
||||||
|
const [volume, setVolume] = useState(1);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [status, setStatus] = useState('Enter text to convert to speech');
|
||||||
|
const [isSpeaking, setIsSpeaking] = useState(false);
|
||||||
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [debugLogs, setDebugLogs] = useState([]);
|
||||||
|
const [showDebug, setShowDebug] = useState(false);
|
||||||
|
|
||||||
|
// Refs
|
||||||
|
const synthRef = useRef(null);
|
||||||
|
const utteranceRef = useRef(null);
|
||||||
|
const audioRef = useRef(null);
|
||||||
|
const textareaRef = useRef(null);
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
const addDebugLog = (message) => {
|
||||||
|
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
|
||||||
|
const logMessage = `${timestamp}: ${message}`;
|
||||||
|
setDebugLogs(prev => [...prev.slice(-100), logMessage]);
|
||||||
|
console.debug(logMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize speech synthesis
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
synthRef.current = window.speechSynthesis;
|
||||||
|
|
||||||
|
const loadVoices = () => {
|
||||||
|
const availableVoices = synthRef.current.getVoices();
|
||||||
|
setVoices(availableVoices);
|
||||||
|
if (availableVoices.length > 0) {
|
||||||
|
setSelectedVoice(availableVoices[0].name);
|
||||||
|
}
|
||||||
|
addDebugLog(`Loaded ${availableVoices.length} voices`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Chrome loads voices asynchronously
|
||||||
|
synthRef.current.onvoiceschanged = loadVoices;
|
||||||
|
loadVoices();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (synthRef.current) {
|
||||||
|
synthRef.current.onvoiceschanged = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle speaking
|
||||||
|
const handleSpeak = () => {
|
||||||
|
if (!text.trim()) {
|
||||||
|
setError('Please enter some text to speak');
|
||||||
|
setStatus('Please enter some text to speak');
|
||||||
|
addDebugLog('No text provided for speech');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!synthRef.current) {
|
||||||
|
setError('Speech synthesis not supported in this browser');
|
||||||
|
setStatus('Speech synthesis not supported');
|
||||||
|
addDebugLog('Speech synthesis API not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPaused) {
|
||||||
|
synthRef.current.resume();
|
||||||
|
setIsPaused(false);
|
||||||
|
setIsSpeaking(true);
|
||||||
|
setStatus('Resumed speaking');
|
||||||
|
addDebugLog('Resumed speech playback');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any current speech
|
||||||
|
synthRef.current.cancel();
|
||||||
|
|
||||||
|
// Create a new utterance
|
||||||
|
const utterance = new SpeechSynthesisUtterance(text);
|
||||||
|
utteranceRef.current = utterance;
|
||||||
|
|
||||||
|
// Set utterance properties
|
||||||
|
const selectedVoiceObj = voices.find(v => v.name === selectedVoice);
|
||||||
|
if (selectedVoiceObj) {
|
||||||
|
utterance.voice = selectedVoiceObj;
|
||||||
|
addDebugLog(`Using voice: ${selectedVoiceObj.name} (${selectedVoiceObj.lang})`);
|
||||||
|
}
|
||||||
|
utterance.pitch = pitch;
|
||||||
|
utterance.rate = rate;
|
||||||
|
utterance.volume = volume;
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
utterance.onstart = () => {
|
||||||
|
setIsSpeaking(true);
|
||||||
|
setIsPaused(false);
|
||||||
|
setStatus('Speaking...');
|
||||||
|
addDebugLog('Speech started');
|
||||||
|
};
|
||||||
|
|
||||||
|
utterance.onend = () => {
|
||||||
|
setIsSpeaking(false);
|
||||||
|
setIsPaused(false);
|
||||||
|
setStatus('Finished speaking');
|
||||||
|
addDebugLog('Speech completed');
|
||||||
|
};
|
||||||
|
|
||||||
|
utterance.onerror = (event) => {
|
||||||
|
setIsSpeaking(false);
|
||||||
|
setIsPaused(false);
|
||||||
|
setStatus(`Error occurred: ${event.error}`);
|
||||||
|
addDebugLog(`Speech error: ${event.error}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Speak the utterance
|
||||||
|
synthRef.current.speak(utterance);
|
||||||
|
setStatus('Started speaking...');
|
||||||
|
addDebugLog('Initiated speech synthesis');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePause = () => {
|
||||||
|
if (isSpeaking && !isPaused && synthRef.current) {
|
||||||
|
synthRef.current.pause();
|
||||||
|
setIsPaused(true);
|
||||||
|
setIsSpeaking(false);
|
||||||
|
setStatus('Paused');
|
||||||
|
addDebugLog('Speech paused');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStop = () => {
|
||||||
|
if (synthRef.current) {
|
||||||
|
synthRef.current.cancel();
|
||||||
|
setIsSpeaking(false);
|
||||||
|
setIsPaused(false);
|
||||||
|
setStatus('Stopped speaking');
|
||||||
|
addDebugLog('Speech stopped');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
setStatus('Download requires a TTS API service');
|
||||||
|
setError('Download functionality requires a TTS API service');
|
||||||
|
addDebugLog('Download attempted (not implemented)');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy text to clipboard
|
||||||
|
const copyToClipboard = () => {
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
addDebugLog('Text copied to clipboard');
|
||||||
|
}).catch(err => {
|
||||||
|
const errorMsg = 'Failed to copy text to clipboard';
|
||||||
|
addDebugLog(`${errorMsg}: ${err.message}`);
|
||||||
|
setError(errorMsg);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear all inputs and states
|
||||||
|
const clearAll = () => {
|
||||||
|
setText('');
|
||||||
|
setError(null);
|
||||||
|
setStatus('Enter text to convert to speech');
|
||||||
|
|
||||||
|
if (synthRef.current) {
|
||||||
|
synthRef.current.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSpeaking(false);
|
||||||
|
setIsPaused(false);
|
||||||
|
|
||||||
|
addDebugLog('All inputs and states cleared');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const clearDebugLogs = () => {
|
||||||
|
setDebugLogs([]);
|
||||||
|
addDebugLog('Debug logs cleared');
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDebug = () => {
|
||||||
|
setShowDebug(!showDebug);
|
||||||
|
addDebugLog(`Debug panel ${showDebug ? 'hidden' : 'shown'}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 max-w-4xl my-6">
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-800">Text to Speech (Using Google)</h1>
|
||||||
|
<Button
|
||||||
|
onClick={toggleDebug}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{showDebug ? 'Hide Debug' : 'Show Debug'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Text Input Section */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<label htmlFor="tts-text" className="block text-sm font-medium text-gray-700">
|
||||||
|
Enter Text
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
disabled={!text || isSpeaking}
|
||||||
|
>
|
||||||
|
{copied ? 'Copied!' : 'Copy Text'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={clearAll}
|
||||||
|
disabled={isSpeaking}
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
id="tts-text"
|
||||||
|
rows={6}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[#6d9e37] focus:border-[#6d9e37] bg-white text-gray-800"
|
||||||
|
placeholder="Type or paste your text here..."
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
disabled={isSpeaking}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="mb-6 p-4 border border-gray-200 rounded-lg">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Voice Selection */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="voice-select" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Select Voice
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="voice-select"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[#6d9e37] focus:border-[#6d9e37]"
|
||||||
|
value={selectedVoice}
|
||||||
|
onChange={(e) => setSelectedVoice(e.target.value)}
|
||||||
|
disabled={isSpeaking}
|
||||||
|
>
|
||||||
|
{voices.length > 0 ? (
|
||||||
|
voices.map((voice) => (
|
||||||
|
<option key={voice.name} value={voice.name}>
|
||||||
|
{voice.name} ({voice.lang})
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<option value="">Loading voices...</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pitch Control */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="pitch" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Pitch: {pitch.toFixed(1)}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="pitch"
|
||||||
|
min="0.5"
|
||||||
|
max="2"
|
||||||
|
step="0.1"
|
||||||
|
value={pitch}
|
||||||
|
onChange={(e) => setPitch(parseFloat(e.target.value))}
|
||||||
|
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-[#6d9e37]"
|
||||||
|
disabled={isSpeaking}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rate Control */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="rate" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Speed: {rate.toFixed(1)}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="rate"
|
||||||
|
min="0.5"
|
||||||
|
max="2"
|
||||||
|
step="0.1"
|
||||||
|
value={rate}
|
||||||
|
onChange={(e) => setRate(parseFloat(e.target.value))}
|
||||||
|
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-[#6d9e37]"
|
||||||
|
disabled={isSpeaking}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Volume Control */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="volume" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Volume: {volume.toFixed(1)}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="volume"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
value={volume}
|
||||||
|
onChange={(e) => setVolume(parseFloat(e.target.value))}
|
||||||
|
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-[#6d9e37]"
|
||||||
|
disabled={isSpeaking}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex flex-wrap gap-3 mb-6">
|
||||||
|
<Button
|
||||||
|
onClick={handleSpeak}
|
||||||
|
disabled={!text.trim() || (isSpeaking && !isPaused)}
|
||||||
|
className="px-6 py-3 text-lg flex-1 min-w-[200px]"
|
||||||
|
>
|
||||||
|
{isPaused ? 'Resume' : 'Speak'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handlePause}
|
||||||
|
disabled={!isSpeaking || isPaused}
|
||||||
|
variant="outline"
|
||||||
|
className="px-6 py-3 text-lg flex-1 min-w-[200px]"
|
||||||
|
>
|
||||||
|
Pause
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleStop}
|
||||||
|
disabled={!isSpeaking && !isPaused}
|
||||||
|
variant="outline"
|
||||||
|
className="px-6 py-3 text-lg flex-1 min-w-[200px]"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleDownload}
|
||||||
|
variant="outline"
|
||||||
|
className="px-6 py-3 text-lg flex-1 min-w-[200px]"
|
||||||
|
>
|
||||||
|
Download Audio
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status */}
|
||||||
|
<div className="mb-6 text-center">
|
||||||
|
<p className={`text-sm ${
|
||||||
|
error ? 'text-red-600' :
|
||||||
|
isSpeaking ? 'text-[#6d9e37]' :
|
||||||
|
isPaused ? 'text-amber-600' :
|
||||||
|
'text-gray-600'
|
||||||
|
}`}>
|
||||||
|
{status}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Display */}
|
||||||
|
{error && (
|
||||||
|
<div className="mt-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<div className="flex items-center text-red-600">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<h3 className="font-medium">Error</h3>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-sm text-red-600">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Debug Section */}
|
||||||
|
{showDebug && (
|
||||||
|
<div className="mt-8 border rounded-lg overflow-hidden">
|
||||||
|
<div className="bg-gray-50 px-4 py-3 border-b flex justify-between items-center">
|
||||||
|
<h3 className="font-medium text-gray-700">Debug Logs</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={clearDebugLogs}
|
||||||
|
className="text-sm">
|
||||||
|
Clear Logs
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const blob = new Blob([debugLogs.join('\n')], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `tts-debug-${new Date().toISOString()}.log`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}}
|
||||||
|
className="text-sm">
|
||||||
|
Export Logs
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-white max-h-60 overflow-y-auto">
|
||||||
|
{debugLogs.length > 0 ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{debugLogs.map((log, index) => (
|
||||||
|
<div key={index} className="text-xs font-mono text-gray-800 border-b border-gray-100 pb-1">
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500">No debug logs yet. Interactions will appear here.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FreeTextToSpeech;
|
|
@ -0,0 +1,375 @@
|
||||||
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
|
||||||
|
const TTS_API_ENDPOINT = 'https://tts41-nhdtuisbdhcvdth.siliconpin.com/tts';
|
||||||
|
|
||||||
|
export default function TextToSpeech() {
|
||||||
|
// State management
|
||||||
|
const [text, setText] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [audioUrl, setAudioUrl] = useState(null);
|
||||||
|
const [status, setStatus] = useState('Enter text to convert to speech');
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [debugLogs, setDebugLogs] = useState([]);
|
||||||
|
const [showDebug, setShowDebug] = useState(false);
|
||||||
|
|
||||||
|
// Refs
|
||||||
|
const audioRef = useRef(null);
|
||||||
|
const textareaRef = useRef(null);
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
const addDebugLog = useCallback((message) => {
|
||||||
|
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
|
||||||
|
const logMessage = `${timestamp}: ${message}`;
|
||||||
|
setDebugLogs(prev => [...prev.slice(-100), logMessage]);
|
||||||
|
console.debug(logMessage);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Clean up audio URL on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (audioUrl) {
|
||||||
|
URL.revokeObjectURL(audioUrl);
|
||||||
|
addDebugLog('Component unmounting - revoking audio URL');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [audioUrl, addDebugLog]);
|
||||||
|
|
||||||
|
// Handle text to speech conversion
|
||||||
|
const convertToSpeech = useCallback(async () => {
|
||||||
|
if (!text.trim()) {
|
||||||
|
setError('Please enter some text to convert');
|
||||||
|
addDebugLog('No text provided for conversion');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setStatus('Converting text to speech...');
|
||||||
|
setError(null);
|
||||||
|
addDebugLog(`Starting TTS conversion for text: "${text.substring(0, 20)}..."`);
|
||||||
|
|
||||||
|
const response = await fetch(TTS_API_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ text: text.trim() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorMsg = `API returned error status: ${response.status}`;
|
||||||
|
addDebugLog(errorMsg);
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioBlob = await response.blob();
|
||||||
|
const url = URL.createObjectURL(audioBlob);
|
||||||
|
|
||||||
|
setAudioUrl(url);
|
||||||
|
setStatus('Conversion complete. Ready to play or download.');
|
||||||
|
addDebugLog(`TTS conversion successful. Audio blob size: ${(audioBlob.size / 1024).toFixed(2)} KB`);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err.message.includes('Failed to fetch')
|
||||||
|
? 'Network error: Could not connect to the TTS server'
|
||||||
|
: err.message;
|
||||||
|
addDebugLog(`Error during TTS conversion: ${errorMsg}`);
|
||||||
|
setError(errorMsg);
|
||||||
|
setStatus('Conversion failed');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
addDebugLog('TTS conversion process completed');
|
||||||
|
}
|
||||||
|
}, [text, addDebugLog]);
|
||||||
|
|
||||||
|
// Handle audio download
|
||||||
|
const downloadAudio = useCallback(() => {
|
||||||
|
if (!audioUrl) return;
|
||||||
|
|
||||||
|
addDebugLog('Initiating audio download');
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = audioUrl;
|
||||||
|
a.download = `tts-output-${new Date().toISOString().slice(0, 10)}.wav`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
setStatus('Audio download started');
|
||||||
|
addDebugLog('Audio download initiated');
|
||||||
|
}, [audioUrl, addDebugLog]);
|
||||||
|
|
||||||
|
// Handle audio playback
|
||||||
|
const togglePlayback = useCallback(() => {
|
||||||
|
if (!audioUrl) return;
|
||||||
|
|
||||||
|
if (isPlaying) {
|
||||||
|
audioRef.current.pause();
|
||||||
|
audioRef.current.currentTime = 0;
|
||||||
|
setIsPlaying(false);
|
||||||
|
setStatus('Playback stopped');
|
||||||
|
addDebugLog('Playback stopped');
|
||||||
|
} else {
|
||||||
|
audioRef.current.play()
|
||||||
|
.then(() => {
|
||||||
|
setIsPlaying(true);
|
||||||
|
setStatus('Playing audio...');
|
||||||
|
addDebugLog('Playback started');
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
setError('Failed to play audio');
|
||||||
|
addDebugLog(`Playback error: ${err.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [audioUrl, isPlaying, addDebugLog]);
|
||||||
|
|
||||||
|
// Handle audio playback end
|
||||||
|
const handleAudioEnd = useCallback(() => {
|
||||||
|
setIsPlaying(false);
|
||||||
|
setStatus('Playback finished');
|
||||||
|
addDebugLog('Playback finished naturally');
|
||||||
|
}, [addDebugLog]);
|
||||||
|
|
||||||
|
// Copy text to clipboard
|
||||||
|
const copyToClipboard = useCallback(() => {
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
addDebugLog('Text copied to clipboard');
|
||||||
|
}).catch(err => {
|
||||||
|
const errorMsg = 'Failed to copy text to clipboard';
|
||||||
|
addDebugLog(`${errorMsg}: ${err.message}`);
|
||||||
|
setError(errorMsg);
|
||||||
|
});
|
||||||
|
}, [text, addDebugLog]);
|
||||||
|
|
||||||
|
// Clear all inputs and states
|
||||||
|
const clearAll = useCallback(() => {
|
||||||
|
setText('');
|
||||||
|
setError(null);
|
||||||
|
setStatus('Enter text to convert to speech');
|
||||||
|
|
||||||
|
if (audioUrl) {
|
||||||
|
URL.revokeObjectURL(audioUrl);
|
||||||
|
setAudioUrl(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlaying) {
|
||||||
|
audioRef.current.pause();
|
||||||
|
audioRef.current.currentTime = 0;
|
||||||
|
setIsPlaying(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
addDebugLog('All inputs and states cleared');
|
||||||
|
}, [audioUrl, isPlaying, addDebugLog]);
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const clearDebugLogs = useCallback(() => {
|
||||||
|
setDebugLogs([]);
|
||||||
|
addDebugLog('Debug logs cleared');
|
||||||
|
}, [addDebugLog]);
|
||||||
|
|
||||||
|
const toggleDebug = useCallback(() => {
|
||||||
|
setShowDebug(!showDebug);
|
||||||
|
addDebugLog(`Debug panel ${showDebug ? 'hidden' : 'shown'}`);
|
||||||
|
}, [showDebug, addDebugLog]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 max-w-4xl my-6">
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<div className='flex flex-col space-y-2'>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-800">Text to Speech (Using our API)</h1>
|
||||||
|
<p className='text-gray-500'>Limitation: 5 Req / Min & 50 Req / Day </p>
|
||||||
|
<p className='text-gray-500'>Need More Purchase from our <a href="/services" className='text-[#6d9e37] font-bold'>Services</a></p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={toggleDebug}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{showDebug ? 'Hide Debug' : 'Show Debug'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Text Input Section */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<label htmlFor="tts-text" className="block text-sm font-medium text-gray-700">
|
||||||
|
Enter Text
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
disabled={!text || isLoading}
|
||||||
|
>
|
||||||
|
{copied ? 'Copied!' : 'Copy Text'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={clearAll}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
id="tts-text"
|
||||||
|
rows={6}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[#6d9e37] focus:border-[#6d9e37] bg-white text-gray-800"
|
||||||
|
placeholder="Type or paste your text here..."
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
disabled={isLoading}
|
||||||
|
maxLength={100}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex flex-wrap gap-3 mb-6">
|
||||||
|
<Button
|
||||||
|
onClick={convertToSpeech}
|
||||||
|
disabled={!text.trim() || isLoading}
|
||||||
|
className="px-6 py-3 text-lg flex-1 min-w-[200px]"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<span className="flex items-center justify-center">
|
||||||
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Converting...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'Convert to Speech'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={togglePlayback}
|
||||||
|
disabled={!audioUrl || isLoading}
|
||||||
|
variant="outline"
|
||||||
|
className="px-6 py-3 text-lg flex-1 min-w-[200px]"
|
||||||
|
>
|
||||||
|
{isPlaying ? (
|
||||||
|
<span className="flex items-center justify-center">
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
Stop Playback
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center justify-center">
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
Play Audio
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={downloadAudio}
|
||||||
|
disabled={!audioUrl || isLoading}
|
||||||
|
variant="outline"
|
||||||
|
className="px-6 py-3 text-lg flex-1 min-w-[200px]"
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center">
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||||
|
</svg>
|
||||||
|
Download Audio
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status */}
|
||||||
|
<div className="mb-6 text-center">
|
||||||
|
<p className={`text-sm ${
|
||||||
|
error ? 'text-red-600' :
|
||||||
|
isLoading ? 'text-[#6d9e37]' :
|
||||||
|
isPlaying ? 'text-[#6d9e37]' :
|
||||||
|
'text-gray-600'
|
||||||
|
}`}>
|
||||||
|
{status}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Display */}
|
||||||
|
{error && (
|
||||||
|
<div className="mt-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<div className="flex items-center text-red-600">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<h3 className="font-medium">Error</h3>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-sm text-red-600">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Audio Player (hidden) */}
|
||||||
|
<audio
|
||||||
|
ref={audioRef}
|
||||||
|
src={audioUrl}
|
||||||
|
onEnded={handleAudioEnd}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Debug Section */}
|
||||||
|
{showDebug && (
|
||||||
|
<div className="mt-8 border rounded-lg overflow-hidden">
|
||||||
|
<div className="bg-gray-50 px-4 py-3 border-b flex justify-between items-center">
|
||||||
|
<h3 className="font-medium text-gray-700">Debug Logs</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={clearDebugLogs}
|
||||||
|
className="text-sm">
|
||||||
|
Clear Logs
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const blob = new Blob([debugLogs.join('\n')], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `tts-debug-${new Date().toISOString()}.log`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}}
|
||||||
|
className="text-sm">
|
||||||
|
Export Logs
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-white max-h-60 overflow-y-auto">
|
||||||
|
{debugLogs.length > 0 ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{debugLogs.map((log, index) => (
|
||||||
|
<div key={index} className="text-xs font-mono text-gray-800 border-b border-gray-100 pb-1">
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500">No debug logs yet. Interactions will appear here.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../../components/ui/card";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
export default function ToolsCard({key, toolName, toolSlug, toolFeatures, toolImg}) {
|
||||||
|
// console.log('tool data', {key, toolName, toolSlug, toolFeatures, toolImg})
|
||||||
|
return(
|
||||||
|
<>
|
||||||
|
<Card className="flex flex-col justify-between hover:shadow-[0_0_10px_#6d9e37] duration-500">
|
||||||
|
<img className="object-cover aspect-video rounded-t-md" src={toolImg} alt={toolName} />
|
||||||
|
|
||||||
|
<CardContent className="flex flex-col flex-1">
|
||||||
|
<CardTitle className="py-2">{toolName}</CardTitle>
|
||||||
|
<hr className="border-b-2 border-[#6d9e37]" />
|
||||||
|
|
||||||
|
|
||||||
|
<ul className="space-y-2 text-sm mb-4 mt-2">
|
||||||
|
{
|
||||||
|
toolFeatures.map((feature, index) => (
|
||||||
|
<li key={index} className="flex items-start">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
|
||||||
|
<span className='text-zinc-600'>{feature}</span>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="mt-auto">
|
||||||
|
<a href={`/web-tools/${toolSlug}`}>
|
||||||
|
<Button onClick={() => window.location.href=`/web-tools/${toolSlug}`} className="w-full">View</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ export default function TopicDetail(props) {
|
||||||
|
|
||||||
const shareUrl = typeof window !== 'undefined' ? window.location.href : '';
|
const shareUrl = typeof window !== 'undefined' ? window.location.href : '';
|
||||||
const title = props.topic.title;
|
const title = props.topic.title;
|
||||||
const text = `Check out this article: ${title}`;
|
const text = `Check out this Topic: ${title}`;
|
||||||
|
|
||||||
const shareOnSocialMedia = (platform) => {
|
const shareOnSocialMedia = (platform) => {
|
||||||
switch (platform){
|
switch (platform){
|
||||||
|
@ -108,7 +108,7 @@ export default function TopicDetail(props) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-12">
|
<div className="container mx-auto px-4 py-12">
|
||||||
<article className="max-w-4xl mx-auto">
|
<article className="max-w-4xl mx-auto">
|
||||||
<img src={props.topic.img ? props.topic.img : '/assets/images/thumb-place.jpg'} alt={props.topic.title} className="w-full h-[400px] aspect-video object-cover rounded-lg mb-8 shadow-md" />
|
<img src={props.topic.img ? props.topic.img : '/assets/images/thumb-place.jpg'} alt={props.topic.title} className="w-full object-cover rounded-lg mb-8 shadow-md" />
|
||||||
<h1 className="text-4xl font-bold text-[#6d9e37] mb-6">{props.topic.title}</h1>
|
<h1 className="text-4xl font-bold text-[#6d9e37] mb-6">{props.topic.title}</h1>
|
||||||
|
|
||||||
{/* Enhanced Social Share Buttons */}
|
{/* Enhanced Social Share Buttons */}
|
||||||
|
@ -194,7 +194,7 @@ export default function TopicDetail(props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="font-light mb-8 text-justify prose max-w-none" dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }} ></div>
|
<div id="markdown-content" className="table-scroll-wrapper font-light mb-8 text-justify prose max-w-none" dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }} ></div>
|
||||||
<CommentSystem topicId={props.topic.id}/>
|
<CommentSystem topicId={props.topic.id}/>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,8 +8,10 @@ import { Separator } from './ui/separator';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
|
||||||
import { CustomTabs } from './ui/tabs';
|
import { CustomTabs } from './ui/tabs';
|
||||||
import Loader from "./ui/loader";
|
import Loader from "./ui/loader";
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
const PUBLIC_TOPIC_API_URL = import.meta.env.PUBLIC_TOPIC_API_URL;
|
const PUBLIC_TOPIC_API_URL = import.meta.env.PUBLIC_TOPIC_API_URL;
|
||||||
const PUBLIC_MINIO_UPLOAD_URL = import.meta.env.PUBLIC_MINIO_UPLOAD_URL;
|
const PUBLIC_MINIO_UPLOAD_URL = 'https://file-vault-prod-pdgctwgsnkiy.siliconpin.com/upload';
|
||||||
|
// const PUBLIC_MINIO_UPLOAD_URL = import.meta.env.PUBLIC_MINIO_UPLOAD_URL;
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const slug = urlParams.get('slug');
|
const slug = urlParams.get('slug');
|
||||||
// console.log('find slug from url', slug);
|
// console.log('find slug from url', slug);
|
||||||
|
@ -66,24 +68,29 @@ export default function EditTopic (){
|
||||||
|
|
||||||
// Upload file to MinIO (same as NewTopic)
|
// Upload file to MinIO (same as NewTopic)
|
||||||
const uploadToMinIO = async (file, onProgress) => {
|
const uploadToMinIO = async (file, onProgress) => {
|
||||||
|
const siliconId = Cookies.get('siliconId'); // Get from cookie or anywhere
|
||||||
|
const token = Cookies.get('token'); // Get from cookie or storage
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('bucket', 'siliconpin-uploads');
|
|
||||||
formData.append('folder', 'topic-images');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(PUBLIC_MINIO_UPLOAD_URL, {
|
const response = await fetch(PUBLIC_MINIO_UPLOAD_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
headers: {
|
||||||
credentials: 'include',
|
'x-user-data': siliconId,
|
||||||
|
'Authorization': token,
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Upload failed');
|
throw new Error(`Upload failed: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.url;
|
// console.log('Upload successful:', data);
|
||||||
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -116,13 +123,49 @@ export default function EditTopic (){
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get all commands (same as NewTopic)
|
// Custom <br> insert command
|
||||||
const allCommands = commands.getCommands().map(cmd => {
|
const lineBreakCommand = {
|
||||||
if (cmd.name === 'image') {
|
name: 'linebreak',
|
||||||
return customImageCommand;
|
keyCommand: 'linebreak',
|
||||||
|
buttonProps: { 'aria-label': 'Insert line break' },
|
||||||
|
icon: (
|
||||||
|
|
||||||
|
<svg width="16px" height="16px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M20 7V8.2C20 9.88016 20 10.7202 19.673 11.362C19.3854 11.9265 18.9265 12.3854 18.362 12.673C17.7202 13 16.8802 13 15.2 13H4M4 13L8 9M4 13L8 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>
|
||||||
|
),
|
||||||
|
execute: () => {
|
||||||
|
const textarea = document.querySelector('.w-md-editor-text-input');
|
||||||
|
if (!textarea) return;
|
||||||
|
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
|
||||||
|
const value = formData.content || "";
|
||||||
|
const breakTag = '<br>';
|
||||||
|
|
||||||
|
const newValue = value.slice(0, start) + breakTag + value.slice(end);
|
||||||
|
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
content: newValue
|
||||||
|
}));
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.selectionStart = textarea.selectionEnd = start + breakTag.length;
|
||||||
|
textarea.focus();
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
return cmd;
|
};
|
||||||
});
|
|
||||||
|
|
||||||
|
// Get all commands (same as NewTopic)
|
||||||
|
const allCommands = [
|
||||||
|
...commands.getCommands().map(cmd => {
|
||||||
|
if (cmd.name === 'image') {
|
||||||
|
return customImageCommand;
|
||||||
|
}
|
||||||
|
return cmd;
|
||||||
|
}),
|
||||||
|
lineBreakCommand,
|
||||||
|
];
|
||||||
|
|
||||||
// Handle image URL insertion (same as NewTopic)
|
// Handle image URL insertion (same as NewTopic)
|
||||||
const handleInsertImageUrl = () => {
|
const handleInsertImageUrl = () => {
|
||||||
|
@ -172,7 +215,7 @@ export default function EditTopic (){
|
||||||
setUploadProgress(progress);
|
setUploadProgress(progress);
|
||||||
});
|
});
|
||||||
|
|
||||||
const imgMarkdown = ``;
|
const imgMarkdown = ``;
|
||||||
const textarea = document.querySelector('.w-md-editor-text-input');
|
const textarea = document.querySelector('.w-md-editor-text-input');
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
const startPos = textarea.selectionStart;
|
const startPos = textarea.selectionStart;
|
||||||
|
@ -212,7 +255,7 @@ export default function EditTopic (){
|
||||||
setUploadProgress(progress);
|
setUploadProgress(progress);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
imageUrl = imageUrl.url;
|
||||||
// Prepare payload
|
// Prepare payload
|
||||||
const payload = {
|
const payload = {
|
||||||
...formData,
|
...formData,
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -207,3 +207,15 @@ const contactSchema3 = {
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<script src="https://suvodip35.github.io/my-chatbot-widget/chatbot.min.js"></script>
|
||||||
|
<script>
|
||||||
|
window.chatbotSettings = {
|
||||||
|
apiKey: "",
|
||||||
|
apiUrl: "",
|
||||||
|
allowedDomains: ["127.0.0.1"],
|
||||||
|
themeColor: "#6d9e37",
|
||||||
|
greetingMessage: "Hi! How can I help you today?",
|
||||||
|
chatbotTitle: "Chat with ঌপি AI",
|
||||||
|
autoOpen: true
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -120,7 +120,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="bg-neutral-800 rounded-lg p-6 sm:p-8 border border-neutral-700">
|
<section id="profile-delete" class="bg-neutral-800 rounded-lg p-6 sm:p-8 border border-neutral-700">
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">6. Your Rights</h2>
|
<h2 class="text-2xl font-bold text-white mb-4">6. Your Rights</h2>
|
||||||
<div class="space-y-4 text-neutral-300">
|
<div class="space-y-4 text-neutral-300">
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -40,12 +40,6 @@ const services = [
|
||||||
buyButtonText: "",
|
buyButtonText: "",
|
||||||
buyButtonUrl: ""
|
buyButtonUrl: ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
imageUrl: '/assets/images/services/stt-streaming.png',
|
|
||||||
learnMoreUrl: '',
|
|
||||||
buyButtonText: 'Purchase',
|
|
||||||
buyButtonUrl: '/services/stt-streaming'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
imageUrl: '/assets/images/services/kubernetes-edge.png',
|
imageUrl: '/assets/images/services/kubernetes-edge.png',
|
||||||
learnMoreUrl: '/services/kubernetes',
|
learnMoreUrl: '/services/kubernetes',
|
||||||
|
@ -69,11 +63,23 @@ const services = [
|
||||||
learnMoreUrl: '/services/hire-an-ai-agent',
|
learnMoreUrl: '/services/hire-an-ai-agent',
|
||||||
buyButtonText: '',
|
buyButtonText: '',
|
||||||
buyButtonUrl: ''
|
buyButtonUrl: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
imageUrl: '/assets/images/services/stt-streaming.png',
|
||||||
|
learnMoreUrl: '',
|
||||||
|
buyButtonText: 'Purchase',
|
||||||
|
buyButtonUrl: '/services/stt-streaming'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
imageUrl: '/assets/images/services/deployapp-thumb.jpg',
|
||||||
|
learnMoreUrl: '',
|
||||||
|
buyButtonText: 'Get VPS',
|
||||||
|
buyButtonUrl: '/services/cloud-instance/'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
let data = [];
|
let data = [];
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${PUBLIC_SERVICE_API_URL}?query=all-services-list`);
|
const response = await fetch(`https://siliconpin.com/v1/services/?query=all-services-list`);
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
if (json.success) {
|
if (json.success) {
|
||||||
// data = [...json.data, ...services];
|
// data = [...json.data, ...services];
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import NewKubernetisUtho from "../../components/BuyServices/NewKubernetisUtho";
|
import NewKubernetisUtho from "../../components/BuyServices/NewKubernetisUtho";
|
||||||
---
|
---
|
||||||
<Layout title="Buy VPN | WireGuard | SiliconPin">
|
<Layout title="Deploy Kubernetis | Kubernetis | SiliconPin">
|
||||||
<NewKubernetisUtho client:load />
|
<NewKubernetisUtho client:load />
|
||||||
</Layout>
|
</Layout>
|
|
@ -0,0 +1,68 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Image Upload Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.upload-container {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.file-input-wrapper {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.file-label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.upload-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.note {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<h1>Image Upload Test</h1>
|
||||||
|
|
||||||
|
<div class="upload-container">
|
||||||
|
<!-- Pure HTML form with direct submission -->
|
||||||
|
<form
|
||||||
|
action="/v1/check/"
|
||||||
|
method="POST"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
>
|
||||||
|
|
||||||
|
<input type="submit" value="Upload Image" class="upload-btn">
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
import Layout from "../layouts/Layout.astro"
|
||||||
|
import TestFileUpload from "../components/TestFileUpload"
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<TestFileUpload client:load />
|
||||||
|
</Layout>
|
|
@ -2,7 +2,7 @@
|
||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import TopicsList from "../../components/Topics";
|
import TopicsList from "../../components/Topics";
|
||||||
|
|
||||||
const TOPIC_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/';
|
const TOPIC_API_URL = 'https://siliconpin.com/v1/topics/';
|
||||||
let topics = [];
|
let topics = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
import ToolsCard from "../../components/Tools/ToolsCard";
|
||||||
|
let toolsData = [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"toolName": "Speech To Text",
|
||||||
|
"slug": "speech-to-text",
|
||||||
|
"img": "https://images.unsplash.com/photo-1554224155-6726b3ff858f?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80",
|
||||||
|
"features": [
|
||||||
|
"Real-time transcription",
|
||||||
|
"Multiple API integrations (Whisper, Vosk)",
|
||||||
|
"Audio recording capability",
|
||||||
|
"Export transcriptions as text files",
|
||||||
|
"Multi-language support",
|
||||||
|
"Punctuation and formatting options"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"toolName": "Image Resize",
|
||||||
|
"slug": "image-resize",
|
||||||
|
"img": "https://images.unsplash.com/photo-1619597455322-4fbbd820250a?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80",
|
||||||
|
"features": [
|
||||||
|
"Bulk image processing",
|
||||||
|
"Preset dimensions for social media",
|
||||||
|
"Maintain aspect ratio",
|
||||||
|
"Quality adjustment",
|
||||||
|
"Multiple output formats (JPG, PNG, WEBP)",
|
||||||
|
"Preview before download",
|
||||||
|
"Drag and drop interface"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"toolName": "Text to Speech",
|
||||||
|
"slug": "text-to-speech",
|
||||||
|
"img": "https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80",
|
||||||
|
"features": [
|
||||||
|
"High-quality speech synthesis",
|
||||||
|
"Multiple voice options",
|
||||||
|
"Adjustable playback speed",
|
||||||
|
"Download as WAV audio",
|
||||||
|
"Real-time preview",
|
||||||
|
"Text formatting support",
|
||||||
|
"API integration",
|
||||||
|
"Debug and logging features"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
---
|
||||||
|
<Layout title="Web Tools | SiliconPin Tools">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<h2 class="text-2xl font-bold my-4 text-[#6d9e37]">SiliconPin Web Toos</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{
|
||||||
|
toolsData.map((items, index) => (
|
||||||
|
<ToolsCard client:load
|
||||||
|
key={items.id}
|
||||||
|
toolName={items.toolName}
|
||||||
|
toolSlug={items.slug}
|
||||||
|
toolFeatures={items.features}
|
||||||
|
toolImg={items.img}
|
||||||
|
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Layout>
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
import TextToSpeech from "../../components/Tools/TextToSpeech";
|
||||||
|
import FreeTextToSpeech from "../../components/Tools/FreeTextToSpeech";
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<TextToSpeech client:load />
|
||||||
|
<FreeTextToSpeech client:load />
|
||||||
|
</Layout>
|
|
@ -0,0 +1,492 @@
|
||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
---
|
||||||
|
<Layout title="Voice Chat">
|
||||||
|
<div class="container mx-auto max-w-2xl h-screen flex flex-col bg-gray-100 my-6">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="bg-[#6d9e37] text-white p-4 shadow-md">
|
||||||
|
<div class="flex items-center gap-x-4">
|
||||||
|
<div class="w-10 h-10 rounded-full bg-green-500 flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h1 class="font-bold text-lg">Voice Assistant</h1>
|
||||||
|
<p class="text-xs opacity-80" id="status">Ready to record</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chat Messages -->
|
||||||
|
<div id="chatMessages" class="flex-1 max-h-[400px] overflow-y-auto p-4 space-y-3 bg-gray-50">
|
||||||
|
<!-- Welcome message -->
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="bg-white rounded-xl rounded-tl-none py-2 px-4 max-w-xs shadow-sm">
|
||||||
|
<p class="text-gray-800">Hello! Tap the microphone to start talking with me.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Audio Visualizer -->
|
||||||
|
<div id="audioVisualizer" class="h-16 bg-gray-200 mx-4 rounded-lg flex items-center justify-center">
|
||||||
|
<div class="w-full h-8 flex items-center justify-center space-x-1" id="visualizerBars"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="p-4 bg-white border-t border-gray-200 flex justify-center">
|
||||||
|
<button id="startBtn" class="p-3 bg-[#6d9e37] rounded-full text-white hover:bg-[#6d9e37]/50 transition-colors shadow-lg">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button id="stopBtn" disabled class="p-3 bg-red-500 rounded-full text-white hover:bg-red-600 transition-colors shadow-lg ml-4 opacity-50 cursor-not-allowed hidden">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
// DOM elements
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
const chatMessages = document.getElementById('chatMessages');
|
||||||
|
const audioVisualizer = document.getElementById('audioVisualizer');
|
||||||
|
const visualizerBars = document.getElementById('visualizerBars');
|
||||||
|
const statusDisplay = document.getElementById('status');
|
||||||
|
|
||||||
|
// Audio variables
|
||||||
|
let audioContext;
|
||||||
|
let mediaStream;
|
||||||
|
let processor;
|
||||||
|
let recording = false;
|
||||||
|
let audioChunks = [];
|
||||||
|
let audioBlob;
|
||||||
|
let silenceTimer;
|
||||||
|
let lastSoundTime;
|
||||||
|
let activeAudioElement = null;
|
||||||
|
let lastResponseAudio = null;
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const config = {
|
||||||
|
sampleRate: 16000, // 16kHz sample rate
|
||||||
|
numChannels: 1, // Mono
|
||||||
|
bitDepth: 16, // 16-bit
|
||||||
|
silenceThreshold: 0.02, // Threshold for silence detection
|
||||||
|
silenceTimeout: 1500, // Stop after 1.5s of silence
|
||||||
|
maxRecordingTime: 30000 // Max recording time in ms (30 seconds)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create visualizer bars
|
||||||
|
function createVisualizerBars() {
|
||||||
|
visualizerBars.innerHTML = '';
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const bar = document.createElement('div');
|
||||||
|
bar.className = 'bg-gray-300 h-1 w-1 rounded-full transition-all duration-75';
|
||||||
|
bar.style.height = '2px';
|
||||||
|
visualizerBars.appendChild(bar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add message to chat
|
||||||
|
function addMessage(text = null, audioUrl = null, isUser = false, isSystem = false) {
|
||||||
|
const messageDiv = document.createElement('div');
|
||||||
|
messageDiv.className = `flex ${isUser ? 'justify-end' : 'justify-start'} mb-3`;
|
||||||
|
|
||||||
|
const bubble = document.createElement('div');
|
||||||
|
bubble.className = `rounded-xl py-2 px-4 max-w-xs shadow-sm ${isUser ? 'bg-green-100 rounded-tr-none' : isSystem ? 'bg-gray-200 rounded-tl-none' : 'bg-white rounded-tl-none'}`;
|
||||||
|
|
||||||
|
if (audioUrl) {
|
||||||
|
// Audio message with text
|
||||||
|
const audioId = `audio-${Date.now()}`;
|
||||||
|
bubble.innerHTML = `
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
${text ? `<p class="text-gray-800">${text}</p>` : ''}
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button class="play-btn p-2 rounded-full bg-white shadow" data-audio="${audioId}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-2.5">
|
||||||
|
<div class="progress-bar bg-green-600 h-2.5 rounded-full" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-gray-500 time-display">0:00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<audio id="${audioId}" src="${audioUrl}" preload="auto"></audio>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Store reference to the last response audio
|
||||||
|
if (!isUser) {
|
||||||
|
lastResponseAudio = { audioId, element: document.getElementById(audioId) };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Text message
|
||||||
|
bubble.innerHTML = `<p class="text-gray-800">${text}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageDiv.appendChild(bubble);
|
||||||
|
chatMessages.appendChild(messageDiv);
|
||||||
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
|
|
||||||
|
return bubble;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-play the last response audio
|
||||||
|
function playLastResponse() {
|
||||||
|
if (lastResponseAudio && lastResponseAudio.element) {
|
||||||
|
const audioElement = lastResponseAudio.element;
|
||||||
|
const playBtn = document.querySelector(`[data-audio="${lastResponseAudio.audioId}"]`);
|
||||||
|
|
||||||
|
if (playBtn) {
|
||||||
|
// Trigger click on the play button to ensure all UI updates are handled
|
||||||
|
playBtn.click();
|
||||||
|
} else {
|
||||||
|
// Fallback to direct play if button not found
|
||||||
|
audioElement.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup audio playback controls
|
||||||
|
function setupAudioControls() {
|
||||||
|
document.querySelectorAll('.play-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const audioId = this.getAttribute('data-audio');
|
||||||
|
const audioElement = document.getElementById(audioId);
|
||||||
|
const progressBar = this.closest('.flex').querySelector('.progress-bar');
|
||||||
|
const timeDisplay = this.closest('.flex').querySelector('.time-display');
|
||||||
|
|
||||||
|
// Stop any currently playing audio
|
||||||
|
if (activeAudioElement && activeAudioElement !== audioElement) {
|
||||||
|
activeAudioElement.pause();
|
||||||
|
activeAudioElement.currentTime = 0;
|
||||||
|
const activeBtn = document.querySelector(`[data-audio="${activeAudioElement.id}"]`);
|
||||||
|
if (activeBtn) {
|
||||||
|
activeBtn.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
activeBtn.closest('.flex').querySelector('.progress-bar').style.width = '0%';
|
||||||
|
activeBtn.closest('.flex').querySelector('.time-display').textContent = '0:00';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioElement.paused) {
|
||||||
|
// Play audio
|
||||||
|
audioElement.play();
|
||||||
|
activeAudioElement = audioElement;
|
||||||
|
this.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Update progress bar
|
||||||
|
audioElement.addEventListener('timeupdate', function() {
|
||||||
|
const progress = (audioElement.currentTime / audioElement.duration) * 100;
|
||||||
|
progressBar.style.width = `${progress}%`;
|
||||||
|
|
||||||
|
// Update time display
|
||||||
|
const minutes = Math.floor(audioElement.currentTime / 60);
|
||||||
|
const seconds = Math.floor(audioElement.currentTime % 60).toString().padStart(2, '0');
|
||||||
|
timeDisplay.textContent = `${minutes}:${seconds}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset when finished
|
||||||
|
audioElement.addEventListener('ended', function() {
|
||||||
|
this.currentTime = 0;
|
||||||
|
progressBar.style.width = '0%';
|
||||||
|
timeDisplay.textContent = '0:00';
|
||||||
|
btn.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Pause audio
|
||||||
|
audioElement.pause();
|
||||||
|
this.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start recording
|
||||||
|
startBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
statusDisplay.textContent = "Starting microphone...";
|
||||||
|
addMessage("Recording...", null, true);
|
||||||
|
|
||||||
|
// Get microphone access
|
||||||
|
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
|
||||||
|
// Create audio context
|
||||||
|
audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
||||||
|
sampleRate: config.sampleRate
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create processor node
|
||||||
|
processor = audioContext.createScriptProcessor(4096, 1, 1);
|
||||||
|
processor.onaudioprocess = processAudio;
|
||||||
|
|
||||||
|
// Create source and connect
|
||||||
|
const source = audioContext.createMediaStreamSource(mediaStream);
|
||||||
|
source.connect(processor);
|
||||||
|
processor.connect(audioContext.destination);
|
||||||
|
|
||||||
|
// Setup visualization
|
||||||
|
setupVisualizer(source);
|
||||||
|
|
||||||
|
// Start recording
|
||||||
|
recording = true;
|
||||||
|
audioChunks = [];
|
||||||
|
lastSoundTime = Date.now();
|
||||||
|
|
||||||
|
// Start silence detection
|
||||||
|
silenceTimer = setInterval(checkSilence, 200);
|
||||||
|
|
||||||
|
// Start max recording time timer
|
||||||
|
setTimeout(() => {
|
||||||
|
if (recording) {
|
||||||
|
stopRecording();
|
||||||
|
}
|
||||||
|
}, config.maxRecordingTime);
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
startBtn.classList.add('hidden');
|
||||||
|
stopBtn.classList.remove('hidden', 'opacity-50', 'cursor-not-allowed');
|
||||||
|
statusDisplay.textContent = "Listening...";
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
statusDisplay.textContent = "Error: " + error.message;
|
||||||
|
addMessage("Error: " + error.message, null, false, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process audio data with silence detection
|
||||||
|
function processAudio(event) {
|
||||||
|
if (!recording) return;
|
||||||
|
|
||||||
|
const inputData = event.inputBuffer.getChannelData(0);
|
||||||
|
|
||||||
|
// Check for sound activity
|
||||||
|
const isSound = isSoundDetected(inputData);
|
||||||
|
if (isSound) {
|
||||||
|
lastSoundTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to 16-bit PCM
|
||||||
|
const buffer = new ArrayBuffer(inputData.length * 2);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
for (let i = 0, offset = 0; i < inputData.length; i++, offset += 2) {
|
||||||
|
const s = Math.max(-1, Math.min(1, inputData[i]));
|
||||||
|
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
audioChunks.push(new Uint8Array(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple sound detection
|
||||||
|
function isSoundDetected(inputData) {
|
||||||
|
for (let i = 0; i < inputData.length; i++) {
|
||||||
|
if (Math.abs(inputData[i]) > config.silenceThreshold) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for silence timeout
|
||||||
|
function checkSilence() {
|
||||||
|
if (!recording) return;
|
||||||
|
|
||||||
|
const silenceDuration = Date.now() - lastSoundTime;
|
||||||
|
if (silenceDuration > config.silenceTimeout) {
|
||||||
|
stopRecording();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop recording and process
|
||||||
|
async function stopRecording() {
|
||||||
|
if (!recording) return;
|
||||||
|
|
||||||
|
// Clear silence timer
|
||||||
|
clearInterval(silenceTimer);
|
||||||
|
|
||||||
|
// Stop recording
|
||||||
|
recording = false;
|
||||||
|
|
||||||
|
// Disconnect audio nodes
|
||||||
|
if (processor) {
|
||||||
|
processor.disconnect();
|
||||||
|
processor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop media stream
|
||||||
|
if (mediaStream) {
|
||||||
|
mediaStream.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create WAV file
|
||||||
|
createWavFile();
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
startBtn.classList.remove('hidden');
|
||||||
|
stopBtn.classList.add('hidden');
|
||||||
|
statusDisplay.textContent = "Processing...";
|
||||||
|
|
||||||
|
// Send to API
|
||||||
|
await sendToAPI();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual stop button
|
||||||
|
stopBtn.addEventListener('click', stopRecording);
|
||||||
|
|
||||||
|
// Create WAV file
|
||||||
|
function createWavFile() {
|
||||||
|
const length = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
||||||
|
const result = new Uint8Array(length);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
audioChunks.forEach(chunk => {
|
||||||
|
result.set(chunk, offset);
|
||||||
|
offset += chunk.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const wavHeader = createWaveHeader(result.length, config);
|
||||||
|
const wavData = new Uint8Array(wavHeader.length + result.length);
|
||||||
|
wavData.set(wavHeader, 0);
|
||||||
|
wavData.set(result, wavHeader.length);
|
||||||
|
|
||||||
|
audioBlob = new Blob([wavData], { type: 'audio/wav' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create WAV header
|
||||||
|
function createWaveHeader(dataLength, config) {
|
||||||
|
const byteRate = config.sampleRate * config.numChannels * (config.bitDepth / 8);
|
||||||
|
const blockAlign = config.numChannels * (config.bitDepth / 8);
|
||||||
|
|
||||||
|
const buffer = new ArrayBuffer(44);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
writeString(view, 0, 'RIFF');
|
||||||
|
view.setUint32(4, 36 + dataLength, true);
|
||||||
|
writeString(view, 8, 'WAVE');
|
||||||
|
writeString(view, 12, 'fmt ');
|
||||||
|
view.setUint32(16, 16, true);
|
||||||
|
view.setUint16(20, 1, true);
|
||||||
|
view.setUint16(22, config.numChannels, true);
|
||||||
|
view.setUint32(24, config.sampleRate, true);
|
||||||
|
view.setUint32(28, byteRate, true);
|
||||||
|
view.setUint16(32, blockAlign, true);
|
||||||
|
view.setUint16(34, config.bitDepth, true);
|
||||||
|
writeString(view, 36, 'data');
|
||||||
|
view.setUint32(40, dataLength, true);
|
||||||
|
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to write strings to DataView
|
||||||
|
function writeString(view, offset, string) {
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send audio to API
|
||||||
|
async function sendToAPI() {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('audio', audioBlob, 'recording.wav');
|
||||||
|
|
||||||
|
const response = await fetch('https://vaani-pocndhvgmcnbacgb.siliconpin.com/upload-audio', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('API request failed');
|
||||||
|
|
||||||
|
// Check content type to handle both JSON and audio responses
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
let responseText = "Here's what I think about that";
|
||||||
|
let audioData;
|
||||||
|
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
|
// Handle JSON response with both text and audio
|
||||||
|
const result = await response.json();
|
||||||
|
responseText = result.text || responseText;
|
||||||
|
console.log('responseText', responseText)
|
||||||
|
audioData = result.audio;
|
||||||
|
} else if (contentType.includes('audio/wav')) {
|
||||||
|
// Handle direct audio response
|
||||||
|
audioData = await response.arrayBuffer();
|
||||||
|
} else {
|
||||||
|
throw new Error('Unexpected response type');
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioUrl = URL.createObjectURL(new Blob([new Uint8Array(audioData)], { type: 'audio/wav' }));
|
||||||
|
|
||||||
|
// Update UI with both text and audio
|
||||||
|
addMessage(responseText, audioUrl, false);
|
||||||
|
statusDisplay.textContent = "Ready to record";
|
||||||
|
|
||||||
|
// Setup playback controls
|
||||||
|
setupAudioControls();
|
||||||
|
|
||||||
|
// Auto-play the response
|
||||||
|
setTimeout(playLastResponse, 500);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error:', error);
|
||||||
|
statusDisplay.textContent = "Error: " + error.message;
|
||||||
|
addMessage("Error processing request", null, false, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup audio visualization
|
||||||
|
function setupVisualizer(source) {
|
||||||
|
const analyser = audioContext.createAnalyser();
|
||||||
|
analyser.fftSize = 32;
|
||||||
|
source.connect(analyser);
|
||||||
|
|
||||||
|
const bufferLength = analyser.frequencyBinCount;
|
||||||
|
const dataArray = new Uint8Array(bufferLength);
|
||||||
|
|
||||||
|
createVisualizerBars();
|
||||||
|
const bars = visualizerBars.querySelectorAll('div');
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
if (!recording) return;
|
||||||
|
|
||||||
|
requestAnimationFrame(draw);
|
||||||
|
analyser.getByteFrequencyData(dataArray);
|
||||||
|
|
||||||
|
for (let i = 0; i < bars.length; i++) {
|
||||||
|
const value = dataArray[i % bufferLength] / 255;
|
||||||
|
const height = value * 30;
|
||||||
|
bars[i].style.height = `${Math.max(2, height)}px`;
|
||||||
|
bars[i].style.backgroundColor = `rgb(${Math.floor(value * 100 + 155)}, ${Math.floor(value * 50 + 50)}, 50)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial setup
|
||||||
|
createVisualizerBars();
|
||||||
|
setupAudioControls();
|
||||||
|
</script>
|
180
yarn.lock
180
yarn.lock
|
@ -1234,9 +1234,9 @@
|
||||||
integrity sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==
|
integrity sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==
|
||||||
|
|
||||||
"@types/prop-types@^15.0.0":
|
"@types/prop-types@^15.0.0":
|
||||||
version "15.7.14"
|
version "15.7.15"
|
||||||
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz"
|
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz"
|
||||||
integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==
|
integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==
|
||||||
|
|
||||||
"@types/react-dom@*", "@types/react-dom@^17.0.17 || ^18.0.6 || ^19.0.0":
|
"@types/react-dom@*", "@types/react-dom@^17.0.17 || ^18.0.6 || ^19.0.0":
|
||||||
version "19.0.4"
|
version "19.0.4"
|
||||||
|
@ -1314,6 +1314,25 @@
|
||||||
remark-gfm "~3.0.1"
|
remark-gfm "~3.0.1"
|
||||||
unist-util-visit "^4.1.0"
|
unist-util-visit "^4.1.0"
|
||||||
|
|
||||||
|
"@uiw/react-markdown-preview@^5.1.4":
|
||||||
|
version "5.1.4"
|
||||||
|
resolved "https://registry.npmjs.org/@uiw/react-markdown-preview/-/react-markdown-preview-5.1.4.tgz"
|
||||||
|
integrity sha512-6k13WVNHCEaamz3vh54OQ1tseIXneKlir1+E/VFQBPq8PRod+gwLfYtiitDBWu+ZFttoiKPLZ7flgHrVM+JNOg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.17.2"
|
||||||
|
"@uiw/copy-to-clipboard" "~1.0.12"
|
||||||
|
react-markdown "~9.0.1"
|
||||||
|
rehype-attr "~3.0.1"
|
||||||
|
rehype-autolink-headings "~7.1.0"
|
||||||
|
rehype-ignore "^2.0.0"
|
||||||
|
rehype-prism-plus "2.0.0"
|
||||||
|
rehype-raw "^7.0.0"
|
||||||
|
rehype-rewrite "~4.0.0"
|
||||||
|
rehype-slug "~6.0.0"
|
||||||
|
remark-gfm "~4.0.0"
|
||||||
|
remark-github-blockquote-alert "^1.0.0"
|
||||||
|
unist-util-visit "^5.0.0"
|
||||||
|
|
||||||
"@uiw/react-md-editor@^3.25.6":
|
"@uiw/react-md-editor@^3.25.6":
|
||||||
version "3.25.6"
|
version "3.25.6"
|
||||||
resolved "https://registry.npmjs.org/@uiw/react-md-editor/-/react-md-editor-3.25.6.tgz"
|
resolved "https://registry.npmjs.org/@uiw/react-md-editor/-/react-md-editor-3.25.6.tgz"
|
||||||
|
@ -2964,6 +2983,13 @@ hast-util-heading-rank@^2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/hast" "^2.0.0"
|
"@types/hast" "^2.0.0"
|
||||||
|
|
||||||
|
hast-util-heading-rank@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz"
|
||||||
|
integrity sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==
|
||||||
|
dependencies:
|
||||||
|
"@types/hast" "^3.0.0"
|
||||||
|
|
||||||
hast-util-is-element@^2.0.0:
|
hast-util-is-element@^2.0.0:
|
||||||
version "2.1.3"
|
version "2.1.3"
|
||||||
resolved "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.3.tgz"
|
resolved "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.3.tgz"
|
||||||
|
@ -3029,7 +3055,7 @@ hast-util-raw@^9.0.0:
|
||||||
web-namespaces "^2.0.0"
|
web-namespaces "^2.0.0"
|
||||||
zwitch "^2.0.0"
|
zwitch "^2.0.0"
|
||||||
|
|
||||||
hast-util-select@^5.0.5:
|
hast-util-select@^5.0.5, hast-util-select@~5.0.1:
|
||||||
version "5.0.5"
|
version "5.0.5"
|
||||||
resolved "https://registry.npmjs.org/hast-util-select/-/hast-util-select-5.0.5.tgz"
|
resolved "https://registry.npmjs.org/hast-util-select/-/hast-util-select-5.0.5.tgz"
|
||||||
integrity sha512-QQhWMhgTFRhCaQdgTKzZ5g31GLQ9qRb1hZtDPMqQaOhpLBziWcshUS0uCR5IJ0U1jrK/mxg35fmcq+Dp/Cy2Aw==
|
integrity sha512-QQhWMhgTFRhCaQdgTKzZ5g31GLQ9qRb1hZtDPMqQaOhpLBziWcshUS0uCR5IJ0U1jrK/mxg35fmcq+Dp/Cy2Aw==
|
||||||
|
@ -3071,27 +3097,6 @@ hast-util-select@^6.0.0:
|
||||||
unist-util-visit "^5.0.0"
|
unist-util-visit "^5.0.0"
|
||||||
zwitch "^2.0.0"
|
zwitch "^2.0.0"
|
||||||
|
|
||||||
hast-util-select@~5.0.1:
|
|
||||||
version "5.0.5"
|
|
||||||
resolved "https://registry.npmjs.org/hast-util-select/-/hast-util-select-5.0.5.tgz"
|
|
||||||
integrity sha512-QQhWMhgTFRhCaQdgTKzZ5g31GLQ9qRb1hZtDPMqQaOhpLBziWcshUS0uCR5IJ0U1jrK/mxg35fmcq+Dp/Cy2Aw==
|
|
||||||
dependencies:
|
|
||||||
"@types/hast" "^2.0.0"
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
bcp-47-match "^2.0.0"
|
|
||||||
comma-separated-tokens "^2.0.0"
|
|
||||||
css-selector-parser "^1.0.0"
|
|
||||||
direction "^2.0.0"
|
|
||||||
hast-util-has-property "^2.0.0"
|
|
||||||
hast-util-to-string "^2.0.0"
|
|
||||||
hast-util-whitespace "^2.0.0"
|
|
||||||
not "^0.1.0"
|
|
||||||
nth-check "^2.0.0"
|
|
||||||
property-information "^6.0.0"
|
|
||||||
space-separated-tokens "^2.0.0"
|
|
||||||
unist-util-visit "^4.0.0"
|
|
||||||
zwitch "^2.0.0"
|
|
||||||
|
|
||||||
hast-util-to-html@^8.0.0:
|
hast-util-to-html@^8.0.0:
|
||||||
version "8.0.4"
|
version "8.0.4"
|
||||||
resolved "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz"
|
resolved "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz"
|
||||||
|
@ -4561,16 +4566,7 @@ micromark-util-resolve-all@^2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
micromark-util-types "^2.0.0"
|
micromark-util-types "^2.0.0"
|
||||||
|
|
||||||
micromark-util-sanitize-uri@^1.0.0:
|
micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0:
|
||||||
version "1.2.0"
|
|
||||||
resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz"
|
|
||||||
integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==
|
|
||||||
dependencies:
|
|
||||||
micromark-util-character "^1.0.0"
|
|
||||||
micromark-util-encode "^1.0.0"
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
|
|
||||||
micromark-util-sanitize-uri@^1.1.0:
|
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz"
|
resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz"
|
||||||
integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==
|
integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==
|
||||||
|
@ -5471,6 +5467,22 @@ react-markdown@~8.0.0:
|
||||||
unist-util-visit "^4.0.0"
|
unist-util-visit "^4.0.0"
|
||||||
vfile "^5.0.0"
|
vfile "^5.0.0"
|
||||||
|
|
||||||
|
react-markdown@~9.0.1:
|
||||||
|
version "9.0.3"
|
||||||
|
resolved "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.3.tgz"
|
||||||
|
integrity sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==
|
||||||
|
dependencies:
|
||||||
|
"@types/hast" "^3.0.0"
|
||||||
|
devlop "^1.0.0"
|
||||||
|
hast-util-to-jsx-runtime "^2.0.0"
|
||||||
|
html-url-attributes "^3.0.0"
|
||||||
|
mdast-util-to-hast "^13.0.0"
|
||||||
|
remark-parse "^11.0.0"
|
||||||
|
remark-rehype "^11.0.0"
|
||||||
|
unified "^11.0.0"
|
||||||
|
unist-util-visit "^5.0.0"
|
||||||
|
vfile "^6.0.0"
|
||||||
|
|
||||||
react-qr-code@^2.0.15:
|
react-qr-code@^2.0.15:
|
||||||
version "2.0.15"
|
version "2.0.15"
|
||||||
resolved "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.15.tgz"
|
resolved "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.15.tgz"
|
||||||
|
@ -5641,6 +5653,14 @@ rehype-attr@~2.1.0:
|
||||||
unified "~10.1.1"
|
unified "~10.1.1"
|
||||||
unist-util-visit "~4.1.0"
|
unist-util-visit "~4.1.0"
|
||||||
|
|
||||||
|
rehype-attr@~3.0.1:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.npmjs.org/rehype-attr/-/rehype-attr-3.0.3.tgz"
|
||||||
|
integrity sha512-Up50Xfra8tyxnkJdCzLBIBtxOcB2M1xdeKe1324U06RAvSjYm7ULSeoM+b/nYPQPVd7jsXJ9+39IG1WAJPXONw==
|
||||||
|
dependencies:
|
||||||
|
unified "~11.0.0"
|
||||||
|
unist-util-visit "~5.0.0"
|
||||||
|
|
||||||
rehype-autolink-headings@~6.1.1:
|
rehype-autolink-headings@~6.1.1:
|
||||||
version "6.1.1"
|
version "6.1.1"
|
||||||
resolved "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-6.1.1.tgz"
|
resolved "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-6.1.1.tgz"
|
||||||
|
@ -5654,6 +5674,18 @@ rehype-autolink-headings@~6.1.1:
|
||||||
unified "^10.0.0"
|
unified "^10.0.0"
|
||||||
unist-util-visit "^4.0.0"
|
unist-util-visit "^4.0.0"
|
||||||
|
|
||||||
|
rehype-autolink-headings@~7.1.0:
|
||||||
|
version "7.1.0"
|
||||||
|
resolved "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz"
|
||||||
|
integrity sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==
|
||||||
|
dependencies:
|
||||||
|
"@types/hast" "^3.0.0"
|
||||||
|
"@ungap/structured-clone" "^1.0.0"
|
||||||
|
hast-util-heading-rank "^3.0.0"
|
||||||
|
hast-util-is-element "^3.0.0"
|
||||||
|
unified "^11.0.0"
|
||||||
|
unist-util-visit "^5.0.0"
|
||||||
|
|
||||||
rehype-ignore@^1.0.1:
|
rehype-ignore@^1.0.1:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.npmjs.org/rehype-ignore/-/rehype-ignore-1.0.5.tgz"
|
resolved "https://registry.npmjs.org/rehype-ignore/-/rehype-ignore-1.0.5.tgz"
|
||||||
|
@ -5663,6 +5695,15 @@ rehype-ignore@^1.0.1:
|
||||||
unified "^10.1.2"
|
unified "^10.1.2"
|
||||||
unist-util-visit "^4.1.2"
|
unist-util-visit "^4.1.2"
|
||||||
|
|
||||||
|
rehype-ignore@^2.0.0:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.npmjs.org/rehype-ignore/-/rehype-ignore-2.0.2.tgz"
|
||||||
|
integrity sha512-BpAT/3lU9DMJ2siYVD/dSR0A/zQgD6Fb+fxkJd4j+wDVy6TYbYpK+FZqu8eM9EuNKGvi4BJR7XTZ/+zF02Dq8w==
|
||||||
|
dependencies:
|
||||||
|
hast-util-select "^6.0.0"
|
||||||
|
unified "^11.0.0"
|
||||||
|
unist-util-visit "^5.0.0"
|
||||||
|
|
||||||
rehype-parse@^8.0.0, rehype-parse@^8.0.2:
|
rehype-parse@^8.0.0, rehype-parse@^8.0.2:
|
||||||
version "8.0.5"
|
version "8.0.5"
|
||||||
resolved "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.5.tgz"
|
resolved "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.5.tgz"
|
||||||
|
@ -5694,6 +5735,18 @@ rehype-prism-plus@~1.6.1, rehype-prism-plus@1.6.3:
|
||||||
unist-util-filter "^4.0.0"
|
unist-util-filter "^4.0.0"
|
||||||
unist-util-visit "^4.0.0"
|
unist-util-visit "^4.0.0"
|
||||||
|
|
||||||
|
rehype-prism-plus@2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/rehype-prism-plus/-/rehype-prism-plus-2.0.0.tgz"
|
||||||
|
integrity sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ==
|
||||||
|
dependencies:
|
||||||
|
hast-util-to-string "^3.0.0"
|
||||||
|
parse-numeric-range "^1.3.0"
|
||||||
|
refractor "^4.8.0"
|
||||||
|
rehype-parse "^9.0.0"
|
||||||
|
unist-util-filter "^5.0.0"
|
||||||
|
unist-util-visit "^5.0.0"
|
||||||
|
|
||||||
rehype-raw@^6.1.1:
|
rehype-raw@^6.1.1:
|
||||||
version "6.1.1"
|
version "6.1.1"
|
||||||
resolved "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz"
|
resolved "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz"
|
||||||
|
@ -5712,7 +5765,7 @@ rehype-raw@^7.0.0:
|
||||||
hast-util-raw "^9.0.0"
|
hast-util-raw "^9.0.0"
|
||||||
vfile "^6.0.0"
|
vfile "^6.0.0"
|
||||||
|
|
||||||
rehype-rewrite@^4.0.2:
|
rehype-rewrite@^4.0.2, rehype-rewrite@~4.0.0:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.npmjs.org/rehype-rewrite/-/rehype-rewrite-4.0.2.tgz"
|
resolved "https://registry.npmjs.org/rehype-rewrite/-/rehype-rewrite-4.0.2.tgz"
|
||||||
integrity sha512-rjLJ3z6fIV11phwCqHp/KRo8xuUCO8o9bFJCNw5o6O2wlLk6g8r323aRswdGBQwfXPFYeSuZdAjp4tzo6RGqEg==
|
integrity sha512-rjLJ3z6fIV11phwCqHp/KRo8xuUCO8o9bFJCNw5o6O2wlLk6g8r323aRswdGBQwfXPFYeSuZdAjp4tzo6RGqEg==
|
||||||
|
@ -5743,6 +5796,17 @@ rehype-slug@~5.1.0:
|
||||||
unified "^10.0.0"
|
unified "^10.0.0"
|
||||||
unist-util-visit "^4.0.0"
|
unist-util-visit "^4.0.0"
|
||||||
|
|
||||||
|
rehype-slug@~6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz"
|
||||||
|
integrity sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==
|
||||||
|
dependencies:
|
||||||
|
"@types/hast" "^3.0.0"
|
||||||
|
github-slugger "^2.0.0"
|
||||||
|
hast-util-heading-rank "^3.0.0"
|
||||||
|
hast-util-to-string "^3.0.0"
|
||||||
|
unist-util-visit "^5.0.0"
|
||||||
|
|
||||||
rehype-stringify@^10.0.0, rehype-stringify@^10.0.1:
|
rehype-stringify@^10.0.0, rehype-stringify@^10.0.1:
|
||||||
version "10.0.1"
|
version "10.0.1"
|
||||||
resolved "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz"
|
resolved "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz"
|
||||||
|
@ -5781,7 +5845,7 @@ rehype@~12.0.1:
|
||||||
rehype-stringify "^9.0.0"
|
rehype-stringify "^9.0.0"
|
||||||
unified "^10.0.0"
|
unified "^10.0.0"
|
||||||
|
|
||||||
remark-gfm@^4.0.1:
|
remark-gfm@^4.0.1, remark-gfm@~4.0.0:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz"
|
resolved "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz"
|
||||||
integrity sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==
|
integrity sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==
|
||||||
|
@ -5803,6 +5867,13 @@ remark-gfm@~3.0.1:
|
||||||
micromark-extension-gfm "^2.0.0"
|
micromark-extension-gfm "^2.0.0"
|
||||||
unified "^10.0.0"
|
unified "^10.0.0"
|
||||||
|
|
||||||
|
remark-github-blockquote-alert@^1.0.0:
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.npmjs.org/remark-github-blockquote-alert/-/remark-github-blockquote-alert-1.3.1.tgz"
|
||||||
|
integrity sha512-OPNnimcKeozWN1w8KVQEuHOxgN3L4rah8geMOLhA5vN9wITqU4FWD+G26tkEsCGHiOVDbISx+Se5rGZ+D1p0Jg==
|
||||||
|
dependencies:
|
||||||
|
unist-util-visit "^5.0.0"
|
||||||
|
|
||||||
remark-parse@^10.0.0:
|
remark-parse@^10.0.0:
|
||||||
version "10.0.2"
|
version "10.0.2"
|
||||||
resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz"
|
resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz"
|
||||||
|
@ -6701,7 +6772,7 @@ unicode-trie@^2.0.0:
|
||||||
pako "^0.2.5"
|
pako "^0.2.5"
|
||||||
tiny-inflate "^1.0.0"
|
tiny-inflate "^1.0.0"
|
||||||
|
|
||||||
unified@^10.0.0, unified@~10.1.1:
|
unified@^10.0.0, unified@^10.1.2, unified@~10.1.1:
|
||||||
version "10.1.2"
|
version "10.1.2"
|
||||||
resolved "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz"
|
resolved "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz"
|
||||||
integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==
|
integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==
|
||||||
|
@ -6714,20 +6785,7 @@ unified@^10.0.0, unified@~10.1.1:
|
||||||
trough "^2.0.0"
|
trough "^2.0.0"
|
||||||
vfile "^5.0.0"
|
vfile "^5.0.0"
|
||||||
|
|
||||||
unified@^10.1.2:
|
unified@^11.0.0, unified@^11.0.3, unified@^11.0.4, unified@^11.0.5, unified@~11.0.0:
|
||||||
version "10.1.2"
|
|
||||||
resolved "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz"
|
|
||||||
integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
bail "^2.0.0"
|
|
||||||
extend "^3.0.0"
|
|
||||||
is-buffer "^2.0.0"
|
|
||||||
is-plain-obj "^4.0.0"
|
|
||||||
trough "^2.0.0"
|
|
||||||
vfile "^5.0.0"
|
|
||||||
|
|
||||||
unified@^11.0.0, unified@^11.0.3, unified@^11.0.4, unified@^11.0.5:
|
|
||||||
version "11.0.5"
|
version "11.0.5"
|
||||||
resolved "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz"
|
resolved "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz"
|
||||||
integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==
|
integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==
|
||||||
|
@ -6749,6 +6807,15 @@ unist-util-filter@^4.0.0:
|
||||||
unist-util-is "^5.0.0"
|
unist-util-is "^5.0.0"
|
||||||
unist-util-visit-parents "^5.0.0"
|
unist-util-visit-parents "^5.0.0"
|
||||||
|
|
||||||
|
unist-util-filter@^5.0.0:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-5.0.1.tgz"
|
||||||
|
integrity sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw==
|
||||||
|
dependencies:
|
||||||
|
"@types/unist" "^3.0.0"
|
||||||
|
unist-util-is "^6.0.0"
|
||||||
|
unist-util-visit-parents "^6.0.0"
|
||||||
|
|
||||||
unist-util-find-after@^5.0.0:
|
unist-util-find-after@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz"
|
resolved "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz"
|
||||||
|
@ -6869,6 +6936,15 @@ unist-util-visit@^5.0.0:
|
||||||
unist-util-is "^6.0.0"
|
unist-util-is "^6.0.0"
|
||||||
unist-util-visit-parents "^6.0.0"
|
unist-util-visit-parents "^6.0.0"
|
||||||
|
|
||||||
|
unist-util-visit@~5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz"
|
||||||
|
integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==
|
||||||
|
dependencies:
|
||||||
|
"@types/unist" "^3.0.0"
|
||||||
|
unist-util-is "^6.0.0"
|
||||||
|
unist-util-visit-parents "^6.0.0"
|
||||||
|
|
||||||
universalify@^0.1.0:
|
universalify@^0.1.0:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz"
|
resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz"
|
||||||
|
|
Loading…
Reference in New Issue