import { Renderer } from '@k8slens/extensions' import { KubeEvent } from '@k8slens/extensions/dist/src/common/k8s-api/endpoints'; import { PieChart } from '../components/pie-chart' // import type { KubeObjectAge } from '@k8slens/extensions/dist/src/renderer/components/kube-object/age'; // import type { Link } from "react-router-dom"; // import type { ReactiveDuration } from "@k8slens/extensions/dist/src/renderer/components/duration/reactive-duration"; import React, { useEffect, useState } from "react"; const { Component: { TableHead, TableRow, TableCell, Table, Spinner, Tooltip, DrawerItem }, } = Renderer; const KubeObject = Renderer.K8sApi.KubeObject; class FluxEventsStore extends Renderer.K8sApi.KubeObjectStore { api = Renderer.K8sApi.eventApi; protected filterItemsOnLoad(items: Renderer.K8sApi.KubeEvent[]): Renderer.K8sApi.KubeEvent[] { return items.filter(i => FluxTypes.findIndex(ft => i.involvedObject.kind === ft.kind) !== -1); } } const fluxEventsStore = new FluxEventsStore(); import { gitRepositoryStore, GitRepository } from '../k8s/fluxcd/gitrepository' import { helmChartStore, HelmChart } from '../k8s/fluxcd/helmchart' import { helmRepositoryStore, HelmRepository } from '../k8s/fluxcd/helmrepository' import { helmReleaseStore, HelmRelease } from '../k8s/fluxcd/helmrelease' import { kustomizationStore, Kustomization, kustomizationApi } from '../k8s/fluxcd/kustomization' import { bucketStore, Bucket } from '../k8s/fluxcd/bucket' import { crdStore } from '../k8s/core/crd' import { FluxcdObjectReconcileMenuItem } from '../menus/fluxcd-object-reconcile-menu-item' import { FluxcdObjectSuspendResumeMenuItem } from '../menus/fluxcd-object-suspend-resume-menu-item' import './fluxcd-dashboard.scss' import { formatDuration } from '../utils'; import { makeObservable } from 'mobx'; import { observer } from 'mobx-react'; enum columnId { message = "message", namespace = "namespace", object = "object", type = "type", count = "count", source = "source", age = "age", lastSeen = "last-seen", } interface FluxCDDashboardState { kustomizations: Kustomization[] gitRepositories: GitRepository[] helmReleases: HelmRelease[] helmCharts: HelmChart[] helmRepositories: HelmRepository[] buckets: Bucket[] crds: Renderer.K8sApi.CustomResourceDefinition[] selectedTableRowId: string } interface IApiConf { api: Renderer.K8sApi.KubeApi; state: Renderer.K8sApi.KubeObject[]; stateUpdate: (list: Renderer.K8sApi.KubeObject[]) => void; } @observer export class FluxCDDashboard extends React.Component<{ extension: Renderer.LensExtension }, FluxCDDashboardState> { public readonly state: Readonly = { kustomizations: [], gitRepositories: [], helmReleases: [], helmCharts: [], helmRepositories: [], buckets: [], crds: [], selectedTableRowId: "" } private readonly watches: (() => void)[] = []; private readonly abortController = new AbortController(); constructor(props: { extension: Renderer.LensExtension }) { super(props); makeObservable(this) } componentWillUnmount(): void { this.abortController.abort(); this.watches.forEach(w => { console.log('Unregistering watch'); w() }); this.watches.splice(0, this.watches.length); this.watches.length = 0; } getCrd(kubeObject: Renderer.K8sApi.KubeObject): Renderer.K8sApi.CustomResourceDefinition { const { crds } = this.state if (!kubeObject) { return null } if (!crds) { return null; } return crds.find(crd => crd.spec.names.kind === kubeObject.kind && crd.spec.group === kubeObject.apiVersion.split("/")[0]) } getChart(title: string, objects: Renderer.K8sApi.KubeObject[]) { if (!objects || objects.length === 0) { return null } const crd = this.getCrd(objects[0]) if (!crd) { return null } return
} async componentDidMount() { crdStore.loadAll().then(l => this.setState({ crds: l })); [ kustomizationStore, helmReleaseStore, gitRepositoryStore, helmChartStore, helmRepositoryStore, bucketStore, ].forEach(store => { store.loadAll().then(() => this.watches.push(store.subscribe())) }) // [ // { api: kustomizationApi, state: kustomizations, stateUpdate: (list: Renderer.K8sApi.KubeObject[]) => this.setState({ kustomizations: list }) }, // { api: gitRepositoryApi, state: gitRepositories, stateUpdate: (list: Renderer.K8sApi.KubeObject[]) => this.setState({ gitRepositories: list }) }, // { api: helmReleaseApi, state: helmReleases, stateUpdate: (list: Renderer.K8sApi.KubeObject[]) => this.setState({ helmReleases: list }) }, // { api: helmChartApi, state: helmCharts, stateUpdate: (list: Renderer.K8sApi.KubeObject[]) => this.setState({ helmCharts: list }) }, // { api: helmRepositoryApi, state: helmRepositories, stateUpdate: (list: Renderer.K8sApi.KubeObject[]) => this.setState({ helmRepositories: list }) }, // { api: bucketApi, state: buckets, stateUpdate: (list: Renderer.K8sApi.KubeObject[]) => this.setState({ buckets: list }) }, // ].forEach((apiConf: IApiConf) => { // const something = apiConf.api.watch({ // namespace: "", // abortController: this.abortController, // callback: (data: IKubeWatchEvent) => { // if (!data) { // return // } // if (data.type === 'ERROR') return // const { type, object } = data // const { state: items } = apiConf; // const existingObjectIndex = apiConf.state.findIndex(k => k.metadata.uid === object.metadata.uid) // const existingObject = apiConf.state[existingObjectIndex] // switch (type) { // case 'ADDED': // case 'MODIFIED': // const kubeObject = new KubeObject(data.object) // console.log('Modified [%s](%s) [%s](%s)', // existingObject?.metadata.name, // existingObject?.metadata.resourceVersion, // kubeObject.metadata.name, // kubeObject.metadata.resourceVersion // ) // if (!existingObject) { // items.push(kubeObject) // } else { // items[existingObjectIndex] = kubeObject // } // break; // case 'DELETED': // if (existingObject) { // items.splice(existingObjectIndex, 1) // } // } // apiConf.stateUpdate(items) // } // }) // this.watches.push(something); // }); } render() { // const { gitRepositories, helmReleases, helmCharts, helmRepositories, buckets } = this.state; const getStatusClassName = (obj: any) => { if (obj.spec.suspend) return 'statusSuspended'; if (obj.status?.conditions.find((c: any) => c.type === "Ready").status === "True") return 'statusReady'; if (obj.status?.conditions.find((c: any) => c.type === "Ready").status === "False") return 'statusNotReady'; return 'statusInProgress'; } if (this.state.crds.length === 0) { return
No Flux components found in the cluster
} return

FluxCD Dashboard

{this.getChart('Kustomizations', kustomizationStore.items)} {this.getChart('Helm releases', helmReleaseStore.items)} {this.getChart('Git Repositories', gitRepositoryStore.items)} {this.getChart('Helm Repositories', helmRepositoryStore.items)} {this.getChart('Helm Charts', helmChartStore.items)} {this.getChart('Buckets', bucketStore.items)}

Applications

{kustomizationStore.items.map(k => (

{ e.preventDefault(); Renderer.Navigation.createPageParam({ name: "kube-details", defaultValue: k.selfLink, }) Renderer.Navigation.showDetails(k.selfLink); }}>{k.metadata.name}

{k.kind} {k.spec.sourceRef.kind}:{k.spec.sourceRef.name} {k.spec.path} {k.spec.interval} {k.status.conditions.find((c: any) => c.type === 'Ready').message}
)) } { helmReleaseStore.items.map(h => (

{ e.preventDefault(); Renderer.Navigation.showDetails(h.selfLink) }}>{h.metadata.name}

{h.kind} {h.spec.chart.spec.sourceRef.kind}:{h.spec.chart.spec.sourceRef.name} {h.spec.chart.spec.chart} {h.spec.chart.spec.version} {h.spec.interval} {h.status.conditions.find((c: any) => c.type === 'Ready').message}
)) }

Sources

{gitRepositoryStore.items.map(s => (

{ e.preventDefault(); Renderer.Navigation.createPageParam({ name: "kube-details", defaultValue: s.selfLink, }) Renderer.Navigation.showDetails(s.selfLink); }}>{s.metadata.name}

{s.kind} {s.spec.url} {s.spec.ref.branch || s.spec.ref.tag} {s.spec.interval} {s.status.conditions.find((c: any) => c.type === 'Ready').message}
)) } {helmRepositoryStore.items.map(s => (

{ e.preventDefault(); Renderer.Navigation.showDetails(s.selfLink) }}>{s.metadata.name}

{s.kind} {s.spec.url} {s.spec.interval} {s.status.conditions.find((c: any) => c.type === 'Ready').message}
)) } {helmChartStore.items.map(s => (

{ e.preventDefault(); Renderer.Navigation.showDetails(s.selfLink) }}>{s.metadata.name}

{s.kind} {s.spec.chart} {s.spec.version} {s.spec.interval} {s.status.conditions.find((c: any) => c.type === 'Ready').message}
)) } {bucketStore.items.map(s => (

{ e.preventDefault(); Renderer.Navigation.showDetails(s.selfLink) }}>{s.metadata.name}

{s.kind} {s.spec.url} {s.spec.interval} {s.status.conditions.find((c: any) => c.type === 'Ready').message}
)) }
fluxEventsStore .contextItems .filter(onlyFluxEvents) .sort((a, b) => (new Date(b.lastTimestamp).getTime() || 0) - (new Date(a.lastTimestamp).getTime() || 0))} sortingCallbacks={{ [columnId.namespace]: event => event.getNs(), [columnId.type]: event => event.type, [columnId.object]: event => event.involvedObject.name, [columnId.count]: event => event.count, [columnId.age]: event => -event.getCreationTimestamp(), [columnId.lastSeen]: event => event.lastTimestamp ? -new Date(event.lastTimestamp).getTime() : 0, }} searchFilters={[ event => event.getSearchFields(), event => event.message, event => event.getSource(), event => event.involvedObject.name, ]} renderHeaderTitle="Flux Events" renderTableHeader={[ { title: "Type", className: "type", sortBy: columnId.type, id: columnId.type }, { title: "Message", className: "message", id: columnId.message }, { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, { title: "Involved Object", className: "object", sortBy: columnId.object, id: columnId.object }, { title: "Source", className: "source", id: columnId.source }, { title: "Count", className: "count", sortBy: columnId.count, id: columnId.count }, { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, { title: "Last Seen", className: "last-seen", sortBy: columnId.lastSeen, id: columnId.lastSeen }, ]} renderTableContents={event => { const { involvedObject, type, message } = event; const tooltipId = `message-${event.getId()}`; const isWarning = event.isWarning(); return [ type, { className: isWarning ? "warning" : "", title: ( <> {message} {message} ), }, event.getNs(), <>{`${involvedObject.kind}: ${involvedObject.name}`}, // , event.getSource(), event.count, , , ]; }} />
} } export function KubeEventAge(props: { timestamp: number }): React.ReactElement { const getAge = (ts: number): string => { const diff = Date.now() - new Date(ts).getTime() return formatDuration(diff, true) } const [age, setAge] = useState(getAge(props.timestamp)); useEffect(() => { const timeout = setInterval(() => { setAge(getAge(props.timestamp)); }, 1000); return () => { clearInterval(timeout); } }, []) return <>{age} } const FluxTypes = [ { kind: "Kustomization", apiVersions: ["kustomize.toolkit.fluxcd.io/v1beta1", "kustomize.toolkit.fluxcd.io/v1beta2"] }, { kind: "HelmRelease", apiVersions: ["helm.toolkit.fluxcd.io/v2beta1"] }, { kind: "GitRepository", apiVersions: ["source.toolkit.fluxcd.io/v1beta1", "source.toolkit.fluxcd.io/v1beta2"] }, { kind: "HelmChart", apiVersions: ["source.toolkit.fluxcd.io/v1beta1", "source.toolkit.fluxcd.io/v1beta2"] }, { kind: "HelmRepository", apiVersions: ["source.toolkit.fluxcd.io/v1beta1", "source.toolkit.fluxcd.io/v1beta2"] }, { kind: "Bucket", apiVersions: ["source.toolkit.fluxcd.io/v1beta1", "source.toolkit.fluxcd.io/v1beta2"] }, ] function onlyFluxEvents(event: KubeEvent) { return FluxTypes.findIndex(ft => { return ft.kind === event.involvedObject.kind && ft.apiVersions.includes(event.involvedObject.apiVersion) }) !== -1 }