Merge branch 'main' of github.com:komodorio/helm-dashboard

This commit is contained in:
Andrei Pohilko
2026-01-20 11:22:48 +00:00
16 changed files with 78 additions and 54 deletions

View File

@@ -84,7 +84,7 @@ If your port 8080 is busy, you can specify a different port to use via `--port <
If you need to limit the operations to a specific namespace, please use `--namespace=...` in your command-line. You can specify multiple namespaces, separated by commas. If you need to limit the operations to a specific namespace, please use `--namespace=...` in your command-line. You can specify multiple namespaces, separated by commas.
If you don't want the browser tab to automatically open, add `--no-browser` flag in your command line. If you don't want the browser tab to automatically open, add `--no-browser` flag in your command-line.
If you want to increase the logging verbosity and see all the debug info, use the `--verbose` flag. If you want to increase the logging verbosity and see all the debug info, use the `--verbose` flag.

View File

@@ -421,9 +421,9 @@
} }
}, },
"node_modules/@cypress/request": { "node_modules/@cypress/request": {
"version": "3.0.9", "version": "3.0.10",
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.9.tgz", "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz",
"integrity": "sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==", "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -440,7 +440,7 @@
"json-stringify-safe": "~5.0.1", "json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19", "mime-types": "~2.1.19",
"performance-now": "^2.1.0", "performance-now": "^2.1.0",
"qs": "6.14.0", "qs": "~6.14.1",
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"tough-cookie": "^5.0.0", "tough-cookie": "^5.0.0",
"tunnel-agent": "^0.6.0", "tunnel-agent": "^0.6.0",
@@ -10675,9 +10675,9 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.14.0", "version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
@@ -10967,9 +10967,9 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.9.6", "version": "7.12.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
"integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==", "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cookie": "^1.0.1", "cookie": "^1.0.1",

View File

@@ -43,6 +43,8 @@ export interface ApplicationStatus {
ClusterMode: boolean; ClusterMode: boolean;
CurVer: string; CurVer: string;
LatestVer: string; LatestVer: string;
NoHealth: boolean;
NoLatest: boolean;
} }
export interface KubectlContexts { export interface KubectlContexts {

View File

@@ -16,6 +16,7 @@ import { isNewerVersion } from "../../utils";
import type { LatestChartVersion } from "../../API/interfaces"; import type { LatestChartVersion } from "../../API/interfaces";
import useNavigateWithSearchParams from "../../hooks/useNavigateWithSearchParams"; import useNavigateWithSearchParams from "../../hooks/useNavigateWithSearchParams";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
import { useGetApplicationStatus } from "../../API/other";
type InstalledPackageCardProps = { type InstalledPackageCardProps = {
release: Release; release: Release;
@@ -31,14 +32,17 @@ export default function InstalledPackageCard({
threshold: 0.3, threshold: 0.3,
triggerOnce: true, triggerOnce: true,
}); });
const { data: status } = useGetApplicationStatus();
const { data: latestVersionResult } = useGetLatestVersion(release.chartName, { const { data: latestVersionResult } = useGetLatestVersion(release.chartName, {
queryKey: ["chartName", release.chartName], queryKey: ["chartName", release.chartName],
enabled: !status?.NoLatest,
}); });
const { data: statusData = [], isLoading } = useQuery<ReleaseHealthStatus[]>({ const { data: statusData = [], isLoading } = useQuery<ReleaseHealthStatus[]>({
queryKey: ["resourceStatus", release], queryKey: ["resourceStatus", release],
queryFn: () => apiService.getResourceStatus({ release }), queryFn: () => apiService.getResourceStatus({ release }),
enabled: inView, enabled: inView && !status?.NoHealth,
}); });
const latestVersionData: LatestChartVersion | undefined = const latestVersionData: LatestChartVersion | undefined =

View File

@@ -23,8 +23,10 @@ const LinkWithSearchParams = ({
let prefixedUrl = to; let prefixedUrl = to;
if (!clusterMode) { if (!clusterMode && context) {
prefixedUrl = `/${encodeURIComponent(context)}${to}`; prefixedUrl = `/${encodeURIComponent(context)}${to}`;
} else {
prefixedUrl = to;
} }
const url = `${prefixedUrl}/?${params.toString()}`; const url = `${prefixedUrl}/?${params.toString()}`;

View File

@@ -18,7 +18,6 @@ export default function Tabs({ tabs, selectedTab }: TabsProps) {
const moveTab = (tab: Tab) => { const moveTab = (tab: Tab) => {
upsertSearchParams("tab", tab.value); upsertSearchParams("tab", tab.value);
}; };
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex pb-2"> <div className="flex pb-2">

View File

@@ -5,8 +5,8 @@ import useAlertError from "../../hooks/useAlertError";
import useCustomSearchParams from "../../hooks/useCustomSearchParams"; import useCustomSearchParams from "../../hooks/useCustomSearchParams";
import { useAppContext } from "../../context/AppContext"; import { useAppContext } from "../../context/AppContext";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router";
import apiService from "../../API/apiService"; import apiService from "../../API/apiService";
import useNavigateWithSearchParams from "../../hooks/useNavigateWithSearchParams";
interface FormKeys { interface FormKeys {
name: string; name: string;
@@ -33,7 +33,7 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const alertError = useAlertError(); const alertError = useAlertError();
const { setSelectedRepo } = useAppContext(); const { setSelectedRepo } = useAppContext();
const navigate = useNavigate(); const navigate = useNavigateWithSearchParams();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const addRepository = async () => { const addRepository = async () => {
@@ -58,7 +58,8 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
queryKey: ["helm", "repositories"], queryKey: ["helm", "repositories"],
}); });
setSelectedRepo(formData.name || ""); setSelectedRepo(formData.name || "");
await navigate(`/repository/${formData.name}`, { const path = `/repository/${formData.name}`;
await navigate(path, {
replace: true, replace: true,
}); });
} catch (err) { } catch (err) {
@@ -68,6 +69,13 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
}); });
} finally { } finally {
setIsLoading(false); setIsLoading(false);
setFormData({
name: "",
url: "",
username: "",
password: "",
});
onClose();
} }
}; };

View File

@@ -335,7 +335,9 @@ const Rollback = ({
useRollbackRelease({ useRollbackRelease({
onSuccess: async () => { onSuccess: async () => {
await navigate( await navigate(
`/${namespace}/${chart}/installed/revision/${revisionInt + 1}` `/${namespace}/${chart}/installed/revision/${
installedRevision.revision + 1
}`
); );
window.location.reload(); window.location.reload();
}, },
@@ -387,7 +389,7 @@ const Rollback = ({
rollbackRelease({ rollbackRelease({
ns: namespace, ns: namespace,
name: String(chart), name: String(chart),
revision: release.revision, revision: rollbackRevision,
}); });
}, },
variant: ModalButtonStyle.info, variant: ModalButtonStyle.info,

View File

@@ -104,6 +104,8 @@ export type Status = {
Analytics: boolean; Analytics: boolean;
CacheHitRatio: number; CacheHitRatio: number;
ClusterMode: boolean; ClusterMode: boolean;
NoHealth: boolean;
NoLatest: boolean;
}; };
export type ChartVersion = { export type ChartVersion = {

View File

@@ -14,6 +14,7 @@ import LinkWithSearchParams from "../components/LinkWithSearchParams";
import apiService from "../API/apiService"; import apiService from "../API/apiService";
import { useAppContext } from "../context/AppContext"; import { useAppContext } from "../context/AppContext";
import { useEffect, useEffectEvent } from "react"; import { useEffect, useEffectEvent } from "react";
import { isNewerVersion } from "../utils";
export default function Header() { export default function Header() {
const { clusterMode, setClusterMode } = useAppContext(); const { clusterMode, setClusterMode } = useAppContext();
@@ -76,7 +77,7 @@ export default function Header() {
<ul className="flex w-full items-center md:mt-0 md:flex-row md:justify-between md:border-0 md:text-sm md:font-normal"> <ul className="flex w-full items-center md:mt-0 md:flex-row md:justify-between md:border-0 md:text-sm md:font-normal">
<li> <li>
<LinkWithSearchParams <LinkWithSearchParams
to={"installed"} to={"/installed"}
exclude={["tab"]} exclude={["tab"]}
className={getBtnStyle("installed")} className={getBtnStyle("installed")}
> >
@@ -85,7 +86,7 @@ export default function Header() {
</li> </li>
<li> <li>
<LinkWithSearchParams <LinkWithSearchParams
to={"repository"} to={"/repository"}
exclude={["tab"]} exclude={["tab"]}
end={false} end={false}
className={getBtnStyle("repository")} className={getBtnStyle("repository")}
@@ -124,7 +125,9 @@ export default function Header() {
]} ]}
/> />
</li> </li>
{"v" + statusData?.CurVer !== statusData?.LatestVer ? ( {statusData?.CurVer &&
statusData?.LatestVer &&
isNewerVersion(statusData.CurVer, statusData.LatestVer) ? (
<li className="min-w-[130px]"> <li className="min-w-[130px]">
<a <a
href="https://github.com/komodorio/helm-dashboard/releases" href="https://github.com/komodorio/helm-dashboard/releases"

View File

@@ -1,4 +1,4 @@
import { useMemo, useEffect, useEffectEvent, useCallback } from "react"; import { useMemo, useEffect, useCallback } from "react";
import RepositoriesList from "../components/repository/RepositoriesList"; import RepositoriesList from "../components/repository/RepositoriesList";
import RepositoryViewer from "../components/repository/RepositoryViewer"; import RepositoryViewer from "../components/repository/RepositoryViewer";
@@ -11,7 +11,7 @@ import useNavigateWithSearchParams from "../hooks/useNavigateWithSearchParams";
function RepositoryPage() { function RepositoryPage() {
const { selectedRepo: repoFromParams, context } = useParams(); const { selectedRepo: repoFromParams, context } = useParams();
const navigate = useNavigateWithSearchParams(); const navigate = useNavigateWithSearchParams();
const { setSelectedRepo, selectedRepo } = useAppContext(); const { setSelectedRepo } = useAppContext();
const navigateTo = useCallback( const navigateTo = useCallback(
async (url: string, ...restArgs: NavigateOptions[]) => { async (url: string, ...restArgs: NavigateOptions[]) => {
@@ -31,28 +31,14 @@ function RepositoryPage() {
setSelectedRepo(repoFromParams); setSelectedRepo(repoFromParams);
} }
}, [setSelectedRepo, repoFromParams]); }, [setSelectedRepo, repoFromParams]);
useEffect(() => {
if (selectedRepo && !repoFromParams) {
void navigateTo(`/repository/${selectedRepo}`, {
replace: true,
});
}
}, [selectedRepo, repoFromParams, context, navigateTo]);
const { data: repositories = [], isSuccess } = useGetRepositories(); const { data: repositories = [], isSuccess } = useGetRepositories();
const onSuccess = useEffectEvent(() => {
if (repositories && repositories.length && !repoFromParams) {
handleRepositoryChanged(repositories[0]);
}
});
useEffect(() => { useEffect(() => {
if (repositories.length && isSuccess) { if (repositories.length && isSuccess && !repoFromParams) {
onSuccess(); const firstRepo = repositories[0];
void navigateTo(`/repository/${firstRepo.name}`, { replace: true });
} }
}, [repositories, isSuccess]); }, [repositories, isSuccess, repoFromParams, context, navigateTo]);
const selectedRepository = useMemo(() => { const selectedRepository = useMemo(() => {
if (repoFromParams) { if (repoFromParams) {

View File

@@ -193,9 +193,9 @@
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@cypress/request@^3.0.0": "@cypress/request@^3.0.0":
version "3.0.9" version "3.0.10"
resolved "https://registry.npmjs.org/@cypress/request/-/request-3.0.9.tgz" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.10.tgz#e09c695e8460a82acafe6cfaf089cf2ca06dc054"
integrity sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw== integrity sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==
dependencies: dependencies:
aws-sign2 "~0.7.0" aws-sign2 "~0.7.0"
aws4 "^1.8.0" aws4 "^1.8.0"
@@ -210,7 +210,7 @@
json-stringify-safe "~5.0.1" json-stringify-safe "~5.0.1"
mime-types "~2.1.19" mime-types "~2.1.19"
performance-now "^2.1.0" performance-now "^2.1.0"
qs "6.14.0" qs "~6.14.1"
safe-buffer "^5.1.2" safe-buffer "^5.1.2"
tough-cookie "^5.0.0" tough-cookie "^5.0.0"
tunnel-agent "^0.6.0" tunnel-agent "^0.6.0"
@@ -5602,10 +5602,10 @@ punycode@^2.1.0:
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
qs@6.14.0: qs@~6.14.1:
version "6.14.0" version "6.14.1"
resolved "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz" resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159"
integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==
dependencies: dependencies:
side-channel "^1.1.0" side-channel "^1.1.0"
@@ -5744,9 +5744,9 @@ react-refresh@^0.18.0:
integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==
react-router@^7.9.6: react-router@^7.9.6:
version "7.9.6" version "7.12.0"
resolved "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz" resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.12.0.tgz#459a86862abbedd02e76e686751fe71f9fd73a4f"
integrity sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA== integrity sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==
dependencies: dependencies:
cookie "^1.0.1" cookie "^1.0.1"
set-cookie-parser "^2.6.0" set-cookie-parser "^2.6.0"

View File

@@ -34,6 +34,8 @@ type options struct {
Namespace string `short:"n" long:"namespace" description:"Namespace for HELM operations"` Namespace string `short:"n" long:"namespace" description:"Namespace for HELM operations"`
Devel bool `long:"devel" description:"Include development versions of charts"` Devel bool `long:"devel" description:"Include development versions of charts"`
LocalChart []string `long:"local-chart" description:"Specify location of local chart to include into UI"` LocalChart []string `long:"local-chart" description:"Specify location of local chart to include into UI"`
NoHealth bool `long:"no-health" description:"Disable health checks for installed charts"`
NoLatest bool `long:"no-latest" description:"Disable latest version checks for installed charts"`
} }
func main() { func main() {
@@ -62,6 +64,8 @@ func main() {
NoTracking: opts.NoTracking, NoTracking: opts.NoTracking,
Devel: opts.Devel, Devel: opts.Devel,
LocalCharts: opts.LocalChart, LocalCharts: opts.LocalChart,
NoHealth: opts.NoHealth,
NoLatest: opts.NoLatest,
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())

View File

@@ -148,7 +148,7 @@ func (h *HelmHandler) Resources(c *gin.Context) {
//return //return
} }
if c.Query("health") != "" { // we need to query k8s for health status if c.Query("health") != "" && !h.Data.StatusInfo.NoHealth { // we need to query k8s for health status
app := h.GetApp(c) app := h.GetApp(c)
if app == nil { if app == nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusInternalServerError, err)
@@ -216,6 +216,11 @@ func (h *HelmHandler) RepoLatestVer(c *gin.Context) {
return // sets error inside return // sets error inside
} }
if h.Data.StatusInfo.NoLatest {
c.Status(http.StatusNoContent)
return
}
rep, err := app.Repositories.Containing(qp.Name) rep, err := app.Repositories.Containing(qp.Name)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusInternalServerError, err)

View File

@@ -41,6 +41,8 @@ type StatusInfo struct {
Analytics bool Analytics bool
CacheHitRatio float64 CacheHitRatio float64
ClusterMode bool ClusterMode bool
NoHealth bool
NoLatest bool
} }
func NewDataLayer(ns []string, ver string, cg HelmConfigGetter, devel bool) (*DataLayer, error) { func NewDataLayer(ns []string, ver string, cg HelmConfigGetter, devel bool) (*DataLayer, error) {

View File

@@ -29,6 +29,8 @@ type Server struct {
NoTracking bool NoTracking bool
Devel bool Devel bool
LocalCharts []string LocalCharts []string
NoHealth bool
NoLatest bool
} }
func (s *Server) StartServer(ctx context.Context, cancel context.CancelFunc) (string, utils.ControlChan, error) { func (s *Server) StartServer(ctx context.Context, cancel context.CancelFunc) (string, utils.ControlChan, error) {
@@ -40,6 +42,9 @@ func (s *Server) StartServer(ctx context.Context, cancel context.CancelFunc) (st
data.LocalCharts = s.LocalCharts data.LocalCharts = s.LocalCharts
data.StatusInfo.Analytics = (!s.NoTracking && s.Version != "0.0.0") || utils.EnvAsBool("HD_DEV_ANALYTICS", false) data.StatusInfo.Analytics = (!s.NoTracking && s.Version != "0.0.0") || utils.EnvAsBool("HD_DEV_ANALYTICS", false)
data.StatusInfo.NoHealth = s.NoHealth || utils.EnvAsBool("HD_NO_HEALTH", false)
data.StatusInfo.NoLatest = s.NoLatest || utils.EnvAsBool("HD_NO_LATEST", false)
err = s.detectClusterMode(data) err = s.detectClusterMode(data)
if err != nil { if err != nil {
return "", nil, errorx.Decorate(err, "Failed to detect cluster mode") return "", nil, errorx.Decorate(err, "Failed to detect cluster mode")