modify helm page
This commit is contained in:
@@ -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">
|
||||||
<div className="mt-3 flex gap-2">
|
<button
|
||||||
<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>
|
onClick={() => { setSelectedPreset(preset); setShowDialog(true); setReleaseName(preset.id) }}
|
||||||
<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>
|
className="px-3 py-1.5 text-sm rounded-md border border-gray-300 hover:bg-gray-50 w-full"
|
||||||
</div>
|
>
|
||||||
|
Configure
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Advanced manual configuration */}
|
|
||||||
<div className="pt-2 border-t border-gray-200">
|
|
||||||
<div className="text-sm font-medium text-gray-700 mb-3">Advanced Configuration</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Preset</label>
|
|
||||||
<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>
|
|
||||||
<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" />
|
|
||||||
</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>
|
|
||||||
<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" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Values (YAML, optional)</label>
|
|
||||||
<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" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Modal for Advanced Configuration */}
|
||||||
|
{showDialog && selectedPreset && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg shadow-lg w-full max-w-lg p-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Configure {selectedPreset.name}</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<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-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">Version (optional)</label>
|
||||||
|
<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 className="mt-6 flex justify-end gap-2">
|
||||||
|
<button onClick={() => setShowDialog(false)} className="px-4 py-2 border border-gray-300 rounded-md text-sm">Cancel</button>
|
||||||
|
<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">
|
||||||
|
{isInstalling ? 'Installing...' : 'Install'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user