modify helm page

This commit is contained in:
Ybehrooz
2025-11-09 19:37:17 +03:30
parent 94f766d199
commit 4f39830b39

View File

@@ -17,50 +17,11 @@ type Preset = {
} }
const PRESETS: Preset[] = [ const PRESETS: Preset[] = [
// Databases & Caches { id: 'mysql', name: 'MySQL (Bitnami)', chart: 'mysql', repo: 'https://charts.bitnami.com/bitnami' },
{ id: 'mysql', name: 'MySQL (Bitnami)', chart: 'bitnami/mysql', repo: 'https://charts.bitnami.com/bitnami' }, { id: 'postgresql', name: 'PostgreSQL (Bitnami)', chart: 'postgresql', repo: 'https://charts.bitnami.com/bitnami' },
{ id: 'mariadb', name: 'MariaDB (Bitnami)', chart: 'bitnami/mariadb', repo: 'https://charts.bitnami.com/bitnami' }, { id: 'redis', name: 'Redis (Bitnami)', chart: 'redis', repo: 'https://charts.bitnami.com/bitnami' },
{ id: 'postgresql', name: 'PostgreSQL (Bitnami)', chart: 'bitnami/postgresql', repo: 'https://charts.bitnami.com/bitnami' }, { id: 'grafana', name: 'Grafana (grafana)', chart: 'grafana', repo: 'https://grafana.github.io/helm-charts' },
{ id: 'mongodb', name: 'MongoDB (Bitnami)', chart: 'bitnami/mongodb', repo: 'https://charts.bitnami.com/bitnami' },
{ id: 'redis', name: 'Redis (Bitnami)', chart: 'bitnami/redis', repo: 'https://charts.bitnami.com/bitnami' },
{ id: 'minio', name: 'MinIO (MinIO)', chart: 'minio/minio', repo: 'https://charts.min.io/' },
// Messaging & Streaming
{ id: 'rabbitmq', name: 'RabbitMQ (Bitnami)', chart: 'bitnami/rabbitmq', repo: 'https://charts.bitnami.com/bitnami' },
{ id: 'kafka', name: 'Kafka (Bitnami)', chart: 'bitnami/kafka', repo: 'https://charts.bitnami.com/bitnami' },
{ id: 'zookeeper', name: 'Zookeeper (Bitnami)', chart: 'bitnami/zookeeper', repo: 'https://charts.bitnami.com/bitnami' },
{ id: 'nats', name: 'NATS (Bitnami)', chart: 'bitnami/nats', repo: 'https://charts.bitnami.com/bitnami' },
{ id: 'emqx', name: 'EMQX (EMQ)', chart: 'emqx/emqx', repo: 'https://repos.emqx.io/charts' },
// Observability
{ id: 'kube-prometheus-stack', name: 'Kube Prometheus Stack', chart: 'prometheus-community/kube-prometheus-stack', repo: 'https://prometheus-community.github.io/helm-charts' },
{ id: 'prometheus', name: 'Prometheus (prometheus-community)', chart: 'prometheus-community/prometheus', repo: 'https://prometheus-community.github.io/helm-charts' }, { id: 'prometheus', name: 'Prometheus (prometheus-community)', chart: 'prometheus-community/prometheus', repo: 'https://prometheus-community.github.io/helm-charts' },
{ id: 'grafana', name: 'Grafana (grafana)', chart: 'grafana/grafana', repo: 'https://grafana.github.io/helm-charts' },
{ id: 'loki', name: 'Loki (grafana)', chart: 'grafana/loki', repo: 'https://grafana.github.io/helm-charts' },
{ id: 'tempo', name: 'Tempo (grafana)', chart: 'grafana/tempo', repo: 'https://grafana.github.io/helm-charts' },
{ id: 'jaeger', name: 'Jaeger (jaegertracing)', chart: 'jaegertracing/jaeger', repo: 'https://jaegertracing.github.io/helm-charts' },
// Ingress & Certs
{ id: 'ingress-nginx', name: 'NGINX Ingress (ingress-nginx)', chart: 'ingress-nginx/ingress-nginx', repo: 'https://kubernetes.github.io/ingress-nginx' },
{ id: 'cert-manager', name: 'cert-manager (jetstack)', chart: 'jetstack/cert-manager', repo: 'https://charts.jetstack.io' },
// Search & Analytics
{ id: 'elasticsearch', name: 'Elasticsearch (Bitnami)', chart: 'bitnami/elasticsearch', repo: 'https://charts.bitnami.com/bitnami' },
{ id: 'kibana', name: 'Kibana (Bitnami)', chart: 'bitnami/kibana', repo: 'https://charts.bitnami.com/bitnami' },
// Identity & Security
{ id: 'keycloak', name: 'Keycloak (Bitnami)', chart: 'bitnami/keycloak', repo: 'https://charts.bitnami.com/bitnami' },
{ id: 'vault', name: 'HashiCorp Vault', chart: 'hashicorp/vault', repo: 'https://helm.releases.hashicorp.com' },
// CI/CD & GitOps
{ id: 'argo-cd', name: 'Argo CD', chart: 'argo/argo-cd', repo: 'https://argoproj.github.io/argo-helm' },
{ id: 'jenkins', name: 'Jenkins (JenkinsCI)', chart: 'jenkinsci/jenkins', repo: 'https://charts.jenkins.io' },
// Registries & Platforms
{ id: 'harbor', name: 'Harbor (goharbor)', chart: 'goharbor/harbor', repo: 'https://helm.goharbor.io' },
// CMS & Apps
{ id: 'wordpress', name: 'WordPress (Bitnami)', chart: 'bitnami/wordpress', repo: 'https://charts.bitnami.com/bitnami' },
] ]
export default function HelmApps() { export default function HelmApps() {
@@ -70,26 +31,12 @@ export default function HelmApps() {
const [selectedNamespace, setSelectedNamespace] = useState<string>('') const [selectedNamespace, setSelectedNamespace] = useState<string>('')
const [search, setSearch] = useState<string>('') const [search, setSearch] = useState<string>('')
const [presetId, setPresetId] = useState<string>('')
const [releaseName, setReleaseName] = useState<string>('') const [releaseName, setReleaseName] = useState<string>('')
const [chart, setChart] = useState<string>('')
const [repo, setRepo] = useState<string>('')
const [version, setVersion] = useState<string>('') const [version, setVersion] = useState<string>('')
const [valuesYaml, setValuesYaml] = useState<string>('') const [selectedPreset, setSelectedPreset] = useState<Preset | null>(null)
const [isInstalling, setIsInstalling] = useState<boolean>(false) const [isInstalling, setIsInstalling] = useState<boolean>(false)
const isInstallingRef = useRef<boolean>(false) const isInstallingRef = useRef<boolean>(false)
const [showDialog, setShowDialog] = useState<boolean>(false)
const applyPreset = (id: string) => {
setPresetId(id)
const p = PRESETS.find(x => x.id === id)
if (p) {
setChart(p.chart)
setRepo(p.repo)
setVersion(p.version || '')
if (!releaseName) setReleaseName(p.id)
if (p.defaults) setValuesYaml(p.defaults)
}
}
const fetchNamespaces = async () => { const fetchNamespaces = async () => {
if (!clusterName) return if (!clusterName) return
@@ -105,25 +52,11 @@ export default function HelmApps() {
} else if (response.status === 401) { } else if (response.status === 401) {
navigate('/login') navigate('/login')
} }
} catch (_err) { } catch {}
}
}
const encodeBase64Utf8 = (input: string): string => {
try { return btoa(unescape(encodeURIComponent(input))) } catch (_err) {
const bytes = new TextEncoder().encode(input)
let binary = ''
const chunkSize = 0x8000
for (let i = 0; i < bytes.length; i += chunkSize) {
const chunk = bytes.subarray(i, i + chunkSize)
binary += String.fromCharCode.apply(null, Array.from(chunk))
}
return btoa(binary)
}
} }
const submitInstall = async () => { const submitInstall = async () => {
if (!clusterName || !selectedNamespace || !releaseName.trim() || !chart.trim()) { if (!clusterName || !selectedNamespace || !releaseName.trim() || !selectedPreset) {
alert('Please fill in cluster, namespace, release, and chart') alert('Please fill in cluster, namespace, release, and chart')
return return
} }
@@ -135,11 +68,10 @@ export default function HelmApps() {
Clustername: clusterName, Clustername: clusterName,
Namespace: selectedNamespace, Namespace: selectedNamespace,
Release: releaseName.trim(), Release: releaseName.trim(),
Chart: chart.trim(), Chart: selectedPreset.chart,
Repo: repo.trim(), Repo: selectedPreset.repo,
} }
if (version.trim()) body.Version = version.trim() if (version.trim()) body.Version = version.trim()
if (valuesYaml.trim()) body.Values = encodeBase64Utf8(valuesYaml)
const res = await fetch('http://localhost:8082/helm_install', { const res = await fetch('http://localhost:8082/helm_install', {
method: 'POST', method: 'POST',
@@ -147,46 +79,12 @@ export default function HelmApps() {
body: JSON.stringify(body) body: JSON.stringify(body)
}) })
if (res.ok) { if (res.ok) {
alert('Helm install request submitted successfully') alert(`Helm install request submitted for ${selectedPreset.name}`)
setShowDialog(false)
} else if (res.status === 401) { } else if (res.status === 401) {
navigate('/login') navigate('/login')
} else { } else {
const data = await res.json().catch(() => ({} as any)) const data = await res.json().catch(() => ({}))
alert('Failed to install: ' + (data.message || 'Unknown error'))
}
} catch (err) {
alert('Failed to install: ' + err)
} finally {
isInstallingRef.current = false
setIsInstalling(false)
}
}
const quickInstall = async (preset: Preset) => {
if (!clusterName || !selectedNamespace) { alert('Select a cluster and namespace first'); return }
if (isInstallingRef.current) return
isInstallingRef.current = true
setIsInstalling(true)
try {
const body: any = {
Clustername: clusterName,
Namespace: selectedNamespace,
Release: `${preset.id}-${Math.random().toString(36).slice(2,7)}`,
Chart: preset.chart,
Repo: preset.repo,
}
if (preset.version) body.Version = preset.version
if (preset.defaults) body.Values = encodeBase64Utf8(preset.defaults)
const res = await fetch('http://localhost:8082/helm_install', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `${localStorage.getItem('auth:token') || ''}` },
body: JSON.stringify(body)
})
if (res.ok) {
alert(`Installing ${preset.name}...`)
} else if (res.status === 401) { navigate('/login') }
else {
const data = await res.json().catch(() => ({} as any))
alert('Failed to install: ' + (data.message || 'Unknown error')) alert('Failed to install: ' + (data.message || 'Unknown error'))
} }
} catch (err) { } catch (err) {
@@ -234,68 +132,51 @@ export default function HelmApps() {
</div> </div>
</div> </div>
</div> </div>
<div className="p-6 space-y-6">
{/* One-click marketplace grid */} <div className="p-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{PRESETS.filter(p => !search || p.name.toLowerCase().includes(search.toLowerCase()) || p.id.includes(search.toLowerCase())).map(preset => ( {PRESETS.filter(p => !search || p.name.toLowerCase().includes(search.toLowerCase()) || p.id.includes(search.toLowerCase())).map(preset => (
<div key={preset.id} className="border border-gray-200 rounded-lg p-4 flex flex-col"> <div key={preset.id} className="border border-gray-200 rounded-lg p-4 flex flex-col">
<div className="font-medium text-gray-900">{preset.name}</div> <div className="font-medium text-gray-900">{preset.name}</div>
<div className="text-xs text-gray-600 mt-1 break-words">{preset.chart}</div> <div className="text-xs text-gray-600 mt-1 break-words">{preset.chart}</div>
<div className="text-xs text-gray-500">{preset.repo}</div> <div className="text-xs text-gray-500">{preset.repo}</div>
<div className="mt-3 flex gap-2"> <div className="mt-3">
<button onClick={() => applyPreset(preset.id)} className="px-3 py-1.5 text-sm rounded-md border border-gray-300 hover:bg-gray-50">Configure</button> <button
<button onClick={() => quickInstall(preset)} disabled={isInstalling || !selectedNamespace} className="px-3 py-1.5 text-sm rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed">One-click Install</button> onClick={() => { setSelectedPreset(preset); setShowDialog(true); setReleaseName(preset.id) }}
className="px-3 py-1.5 text-sm rounded-md border border-gray-300 hover:bg-gray-50 w-full"
>
Configure
</button>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div>
{/* Advanced manual configuration */} {/* Modal for Advanced Configuration */}
<div className="pt-2 border-t border-gray-200"> {showDialog && selectedPreset && (
<div className="text-sm font-medium text-gray-700 mb-3">Advanced Configuration</div> <div className="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="bg-white rounded-lg shadow-lg w-full max-w-lg p-6">
<div> <h3 className="text-lg font-semibold mb-4">Configure {selectedPreset.name}</h3>
<label className="block text-sm font-medium text-gray-700 mb-1">Preset</label> <div className="space-y-4">
<select value={presetId} onChange={(e) => applyPreset(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">Select a preset...</option>
{PRESETS.map(p => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
</div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Release Name</label> <label className="block text-sm font-medium text-gray-700 mb-1">Release Name</label>
<input value={releaseName} onChange={(e) => setReleaseName(e.target.value)} placeholder="e.g., my-mysql" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> <input value={releaseName} onChange={(e) => setReleaseName(e.target.value)} placeholder="e.g., my-app" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Chart</label>
<input value={chart} onChange={(e) => setChart(e.target.value)} placeholder="e.g., bitnami/mysql" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Repository URL</label>
<input value={repo} onChange={(e) => setRepo(e.target.value)} placeholder="e.g., https://charts.bitnami.com/bitnami" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Version (optional)</label> <label className="block text-sm font-medium text-gray-700 mb-1">Version (optional)</label>
<input value={version} onChange={(e) => setVersion(e.target.value)} placeholder="e.g., 13.1.2" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> <input value={version} onChange={(e) => setVersion(e.target.value)} placeholder="e.g., 1.2.3" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500" />
</div> </div>
</div> </div>
<div> <div className="mt-6 flex justify-end gap-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Values (YAML, optional)</label> <button onClick={() => setShowDialog(false)} className="px-4 py-2 border border-gray-300 rounded-md text-sm">Cancel</button>
<textarea value={valuesYaml} onChange={(e) => setValuesYaml(e.target.value)} placeholder="# Paste custom values.yaml here" className="w-full h-48 p-3 text-xs font-mono border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> <button onClick={submitInstall} disabled={isInstalling} className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 text-sm">
</div> {isInstalling ? 'Installing...' : 'Install'}
<div className="flex justify-end">
<button onClick={submitInstall} disabled={isInstalling} className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-sm">
{isInstalling ? 'Installing...' : 'Install via Helm'}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> )}
</div> </div>
) )
} }