mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-10 21:49:19 -06:00
feat/add app monitoring usage
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import monitoringService from "@/server/services/monitoring.service";
|
||||
import clusterService from "@/server/services/node.service";
|
||||
import { getAuthUserSession, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { AppMonitoringUsageModel } from "@/shared/model/app-monitoring-usage.model";
|
||||
import { AppVolumeMonitoringUsageModel } from "@/shared/model/app-volume-monitoring-usage.model";
|
||||
import { NodeResourceModel } from "@/shared/model/node-resource.model";
|
||||
import { ServerActionResult } from "@/shared/model/server-action-error-return.model";
|
||||
@@ -17,4 +18,10 @@ export const getVolumeMonitoringUsage = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
return await monitoringService.getAllAppVolumesUsage();
|
||||
}) as Promise<ServerActionResult<unknown, AppVolumeMonitoringUsageModel[]>>;
|
||||
}) as Promise<ServerActionResult<unknown, AppVolumeMonitoringUsageModel[]>>;
|
||||
|
||||
export const getMonitoringForAllApps = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
return await monitoringService.getMonitoringForAllApps();
|
||||
}) as Promise<ServerActionResult<unknown, AppMonitoringUsageModel[]>>;
|
||||
100
src/app/monitoring/app-monitoring.tsx
Normal file
100
src/app/monitoring/app-monitoring.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Actions } from '@/frontend/utils/nextjs-actions.utils';
|
||||
import { getMonitoringForAllApps, getVolumeMonitoringUsage } from './actions';
|
||||
import { toast } from 'sonner';
|
||||
import FullLoadingSpinner from '@/components/ui/full-loading-spinnter';
|
||||
import { AppVolumeMonitoringUsageModel } from '@/shared/model/app-volume-monitoring-usage.model';
|
||||
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { KubeSizeConverter } from '@/shared/utils/kubernetes-size-converter.utils';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import dataAccess from '@/server/adapter/db.client';
|
||||
import { ProgressIndicator } from '@radix-ui/react-progress';
|
||||
import { AppMonitoringUsageModel } from '@/shared/model/app-monitoring-usage.model';
|
||||
|
||||
export default function AppRessourceMonitoring({
|
||||
appsRessourceUsage
|
||||
}: {
|
||||
appsRessourceUsage?: AppMonitoringUsageModel[]
|
||||
}) {
|
||||
|
||||
|
||||
const [updatedAppUsage, setUpdatedAppUsage] = useState<AppMonitoringUsageModel[] | undefined>(appsRessourceUsage);
|
||||
|
||||
const fetchMonitoringData = async () => {
|
||||
try {
|
||||
const data = await Actions.run(() => getMonitoringForAllApps());
|
||||
setUpdatedAppUsage(data);
|
||||
} catch (ex) {
|
||||
toast.error('An error occurred while fetching current volume usage');
|
||||
console.error('An error occurred while fetching volume nodes', ex);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => fetchMonitoringData(), 10000);
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}, [appsRessourceUsage]);
|
||||
|
||||
if (!updatedAppUsage) {
|
||||
return <Card>
|
||||
<CardContent>
|
||||
<FullLoadingSpinner />
|
||||
</CardContent>
|
||||
</Card>
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>App Ressource Usage</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableCaption>{updatedAppUsage.length} Apps</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Project</TableHead>
|
||||
<TableHead>App</TableHead>
|
||||
<TableHead>CPU</TableHead>
|
||||
<TableHead>RAM</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{updatedAppUsage.map((item, index) => (
|
||||
<TableRow key={item.appId}>
|
||||
<TableCell>{item.projectName}</TableCell>
|
||||
<TableCell>{item.appName}</TableCell>
|
||||
<TableCell>
|
||||
<span className='font-semibold'>{item.cpuUsagePercent.toFixed(3)}%</span> / {item.cpuUsage.toFixed(5)} Cores
|
||||
</TableCell>
|
||||
<TableCell>{KubeSizeConverter.convertBytesToReadableSize(item.ramUsageBytes)}</TableCell>
|
||||
<TableCell>
|
||||
<Link href={`/project/app/${item.appId}`} >
|
||||
<Button variant="ghost" size="sm">
|
||||
<ExternalLink />
|
||||
</Button>
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -31,13 +31,12 @@ import { AppVolumeMonitoringUsageModel } from '@/shared/model/app-volume-monitor
|
||||
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { KubeSizeConverter } from '@/shared/utils/kubernetes-size-converter.utils';
|
||||
import AppVolumeMonitoring from './app-volumes-monitoring';
|
||||
import AppRessourceMonitoring from './app-monitoring';
|
||||
|
||||
export default function ResourcesNodes({
|
||||
resourcesNodes,
|
||||
volumesUsage
|
||||
}: {
|
||||
resourcesNodes?: NodeResourceModel[];
|
||||
volumesUsage?: AppVolumeMonitoringUsageModel[]
|
||||
}) {
|
||||
|
||||
const [updatedNodeRessources, setUpdatedResourcesNodes] = useState<NodeResourceModel[] | undefined>(resourcesNodes);
|
||||
@@ -248,7 +247,6 @@ export default function ResourcesNodes({
|
||||
</Card>
|
||||
</>))
|
||||
}
|
||||
<AppVolumeMonitoring volumesUsage={volumesUsage} />
|
||||
</div >
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,15 +7,22 @@ import ResourceNodes from "./monitoring-nodes";
|
||||
import { NodeResourceModel } from "@/shared/model/node-resource.model";
|
||||
import { AppVolumeMonitoringUsageModel } from "@/shared/model/app-volume-monitoring-usage.model";
|
||||
import monitoringService from "@/server/services/monitoring.service";
|
||||
import AppRessourceMonitoring from "./app-monitoring";
|
||||
import AppVolumeMonitoring from "./app-volumes-monitoring";
|
||||
import { AppMonitoringUsageModel } from "@/shared/model/app-monitoring-usage.model";
|
||||
|
||||
export default async function ResourceNodesInfoPage() {
|
||||
|
||||
await getAuthUserSession();
|
||||
let resourcesNode: NodeResourceModel[] | undefined;
|
||||
let volumesUsage: AppVolumeMonitoringUsageModel[] | undefined;
|
||||
let updatedNodeRessources: AppMonitoringUsageModel[] | undefined;
|
||||
try {
|
||||
resourcesNode = await clusterService.getNodeResourceUsage();
|
||||
volumesUsage = await monitoringService.getAllAppVolumesUsage();
|
||||
[resourcesNode, volumesUsage, updatedNodeRessources] = await Promise.all([
|
||||
clusterService.getNodeResourceUsage(),
|
||||
monitoringService.getAllAppVolumesUsage(),
|
||||
await monitoringService.getMonitoringForAllApps()
|
||||
]);
|
||||
} catch (ex) {
|
||||
// do nothing --> if an error occurs, the ResourceNodes will show a loading spinner and error message
|
||||
}
|
||||
@@ -26,7 +33,11 @@ export default async function ResourceNodesInfoPage() {
|
||||
title={'Monitoring'}
|
||||
subtitle={`View all resources of the nodes which belong to the QuickStack Cluster.`}>
|
||||
</PageTitle>
|
||||
<ResourceNodes resourcesNodes={resourcesNode} volumesUsage={volumesUsage} />
|
||||
<div className="space-y-6">
|
||||
<ResourceNodes resourcesNodes={resourcesNode} />
|
||||
<AppRessourceMonitoring appsRessourceUsage={updatedNodeRessources} />
|
||||
<AppVolumeMonitoring volumesUsage={volumesUsage} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function MonitoringTab({
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className={'px-3 py-1.5 rounded cursor-pointer'}>{selectedPod?.cpuPercent.toFixed(2)}</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -14,12 +14,6 @@ export default async function NodeInfo({ nodeInfos }: { nodeInfos: NodeInfoModel
|
||||
|
||||
const { openConfirmDialog: openDialog } = useConfirmDialog();
|
||||
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
useEffect(() => setBreadcrumbs([
|
||||
{ name: "Settings", url: "/settings/profile" },
|
||||
{ name: "Cluster" },
|
||||
]), []);
|
||||
|
||||
const setNodeStatusClick = async (nodeName: string, schedulable: boolean) => {
|
||||
const confirmation = await openDialog({
|
||||
title: 'Update Node Status',
|
||||
@@ -87,7 +81,7 @@ export default async function NodeInfo({ nodeInfos }: { nodeInfos: NodeInfoModel
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className={nodeInfo.schedulable ? 'text-green-500 font-semibold' : 'text-red-500 font-semibold'}> {nodeInfo.schedulable ? 'Yes' : 'No'}</span>
|
||||
<span className={nodeInfo.schedulable ? 'text-green-500 font-semibold' : 'text-red-500 font-semibold'}> {nodeInfo.schedulable ? 'Yes' : 'No'}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-[350px]">{nodeInfo.schedulable ? 'Node is ready to run containers.' : 'Node ist deactivated. All containers will be scheduled on other nodes.'}</p>
|
||||
|
||||
@@ -7,6 +7,7 @@ import NodeInfo from "./nodeInfo";
|
||||
import AddClusterNodeDialog from "./add-cluster-node-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import paramService, { ParamService } from "@/server/services/param.service";
|
||||
import BreadcrumbSetter from "@/components/breadcrumbs-setter";
|
||||
|
||||
export default async function ClusterInfoPage() {
|
||||
|
||||
@@ -22,6 +23,10 @@ export default async function ClusterInfoPage() {
|
||||
<Button>Add Cluster Node</Button>
|
||||
</AddClusterNodeDialog>
|
||||
</PageTitle>
|
||||
<BreadcrumbSetter items={[
|
||||
{ name: "Settings", url: "/settings/profile" },
|
||||
{ name: "Cluster" },
|
||||
]} />
|
||||
<NodeInfo nodeInfos={nodeInfo} />
|
||||
</div>
|
||||
)
|
||||
|
||||
40
src/app/settings/maintenance/page.tsx
Normal file
40
src/app/settings/maintenance/page.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
'use server'
|
||||
|
||||
import { getAuthUserSession } from "@/server/utils/action-wrapper.utils";
|
||||
import PageTitle from "@/components/custom/page-title";
|
||||
import paramService, { ParamService } from "@/server/services/param.service";
|
||||
import podService from "@/server/services/pod.service";
|
||||
import { Constants } from "@/shared/utils/constants";
|
||||
import s3TargetService from "@/server/services/s3-target.service";
|
||||
import QuickStackVersionInfo from "./qs-version-info";
|
||||
import QuickStackMaintenanceSettings from "./qs-maintenance-settings";
|
||||
import BreadcrumbSetter from "@/components/breadcrumbs-setter";
|
||||
|
||||
export default async function MaintenancePage() {
|
||||
|
||||
await getAuthUserSession();
|
||||
const useCanaryChannel = await paramService.getBoolean(ParamService.USE_CANARY_CHANNEL, false);
|
||||
const qsPodInfos = await podService.getPodsForApp(Constants.QS_NAMESPACE, Constants.QS_APP_NAME);
|
||||
const qsPodInfo = qsPodInfos.find(p => !!p);
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-4 pt-6">
|
||||
<PageTitle
|
||||
title={'Maintenance'}
|
||||
subtitle={`Options to maintain your QuickStack Cluster`}>
|
||||
</PageTitle>
|
||||
<BreadcrumbSetter items={[
|
||||
{
|
||||
name: 'Settings'
|
||||
},
|
||||
{
|
||||
name: 'Maintenance'
|
||||
},
|
||||
]} />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div><QuickStackVersionInfo useCanaryChannel={useCanaryChannel!} /></div>
|
||||
<div><QuickStackMaintenanceSettings qsPodName={qsPodInfo?.podName} /></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { purgeRegistryImages, updateQuickstack, updateRegistry } from "./actions";
|
||||
import { cleanupOldBuildJobs, purgeRegistryImages, updateQuickstack, updateRegistry } from "../server/actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Toast } from "@/frontend/utils/toast.utils";
|
||||
import { useConfirmDialog } from "@/frontend/states/zustand.states";
|
||||
@@ -17,17 +17,13 @@ export default function QuickStackMaintenanceSettings({
|
||||
|
||||
const useConfirm = useConfirmDialog();
|
||||
|
||||
return <>
|
||||
return <div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Maintenance</CardTitle>
|
||||
<CardTitle>Free Up Disk Space</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex gap-4 flex-wrap">
|
||||
|
||||
{qsPodName && <LogsDialog namespace={Constants.QS_NAMESPACE} podName={qsPodName}>
|
||||
<Button variant="secondary" ><SquareTerminal /> Open QuickStack Logs</Button>
|
||||
</LogsDialog>}
|
||||
|
||||
<Button variant="secondary" onClick={async () => {
|
||||
if (await useConfirm.openConfirmDialog({
|
||||
title: 'Purge Images',
|
||||
@@ -38,6 +34,29 @@ export default function QuickStackMaintenanceSettings({
|
||||
}
|
||||
}}><Trash /> Purge Images</Button>
|
||||
|
||||
<Button variant="secondary" onClick={async () => {
|
||||
if (await useConfirm.openConfirmDialog({
|
||||
title: 'Cleanup Old Build Jobs',
|
||||
description: 'This action deletes all old build jobs. Use this action to free up disk space.',
|
||||
okButton: "Cleanup Old Build Jobs"
|
||||
})) {
|
||||
Toast.fromAction(() => cleanupOldBuildJobs());
|
||||
}
|
||||
}}><Trash /> Cleanup Old Build Jobs</Button>
|
||||
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Monitoring & Troubleshooting</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex gap-4 flex-wrap">
|
||||
|
||||
{qsPodName && <LogsDialog namespace={Constants.QS_NAMESPACE} podName={qsPodName}>
|
||||
<Button variant="secondary" ><SquareTerminal /> Open QuickStack Logs</Button>
|
||||
</LogsDialog>}
|
||||
|
||||
<Button variant="secondary" onClick={async () => {
|
||||
if (await useConfirm.openConfirmDialog({
|
||||
title: 'Update Registry',
|
||||
@@ -49,7 +68,6 @@ export default function QuickStackMaintenanceSettings({
|
||||
}}><RotateCcw /> Force Update Registry</Button>
|
||||
|
||||
</CardContent>
|
||||
</Card >
|
||||
|
||||
</>;
|
||||
</Card>
|
||||
</div>;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { purgeRegistryImages, setCanaryChannel, updateQuickstack, updateRegistry } from "./actions";
|
||||
import { purgeRegistryImages, setCanaryChannel, updateQuickstack, updateRegistry } from "../server/actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Toast } from "@/frontend/utils/toast.utils";
|
||||
import { useConfirmDialog } from "@/frontend/states/zustand.states";
|
||||
@@ -11,7 +11,7 @@ import PageTitle from "@/components/custom/page-title";
|
||||
import ProfilePasswordChange from "./profile-password-change";
|
||||
import ToTpSettings from "./totp-settings";
|
||||
import userService from "@/server/services/user.service";
|
||||
import BreadcrumbsSettings from "./profile-breadcrumbs";
|
||||
import BreadcrumbSetter from "@/components/breadcrumbs-setter";
|
||||
|
||||
export default async function ProjectPage() {
|
||||
|
||||
@@ -23,7 +23,10 @@ export default async function ProjectPage() {
|
||||
title={'Profile'}
|
||||
subtitle={`View or edit your Profile information and configure your authentication.`}>
|
||||
</PageTitle>
|
||||
<BreadcrumbsSettings />
|
||||
<BreadcrumbSetter items={[
|
||||
{ name: "Settings", url: "/settings/profile" },
|
||||
{ name: "Profile" },
|
||||
]} />
|
||||
<ProfilePasswordChange />
|
||||
<ToTpSettings totpEnabled={data.twoFaEnabled} />
|
||||
</div>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { deactivate2fa } from "./actions";
|
||||
import { Toast } from "@/frontend/utils/toast.utils";
|
||||
import TotpCreateDialog from "./totp-create-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useBreadcrumbs } from "@/frontend/states/zustand.states";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function BreadcrumbsSettings() {
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
useEffect(() => setBreadcrumbs([
|
||||
{ name: "Settings", url: "/settings/profile" },
|
||||
{ name: "Profile" },
|
||||
]), []);
|
||||
return <></>;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import s3TargetService from "@/server/services/s3-target.service";
|
||||
import S3TargetsTable from "./s3-targets-table";
|
||||
import S3TargetEditOverlay from "./s3-target-edit-overlay";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import BreadcrumbSetter from "@/components/breadcrumbs-setter";
|
||||
|
||||
export default async function S3TargetsPage() {
|
||||
|
||||
@@ -21,6 +22,10 @@ export default async function S3TargetsPage() {
|
||||
<Button>Add S3 Target</Button>
|
||||
</S3TargetEditOverlay>
|
||||
</PageTitle>
|
||||
<BreadcrumbSetter items={[
|
||||
{ name: "Settings", url: "/settings/profile" },
|
||||
{ name: "S3 Targets" },
|
||||
]} />
|
||||
<S3TargetsTable targets={data} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Constants } from "@/shared/utils/constants";
|
||||
import { QsPublicIpv4SettingsModel, qsPublicIpv4SettingsZodModel } from "@/shared/model/qs-public-ipv4-settings.model";
|
||||
import ipAddressFinderAdapter from "@/server/adapter/ip-adress-finder.adapter";
|
||||
import { KubeSizeConverter } from "@/shared/utils/kubernetes-size-converter.utils";
|
||||
import buildService from "@/server/services/build.service";
|
||||
|
||||
export const updateIngressSettings = async (prevState: any, inputData: QsIngressSettingsModel) =>
|
||||
saveFormAction(inputData, qsIngressSettingsZodModel, async (validatedData) => {
|
||||
@@ -75,6 +76,13 @@ export const getConfiguredHostname: () => Promise<ServerActionResult<unknown, st
|
||||
return await paramService.getString(ParamService.QS_SERVER_HOSTNAME);
|
||||
});
|
||||
|
||||
export const cleanupOldBuildJobs = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await buildService.deleteAllFailedOrSuccededBuilds();
|
||||
return new SuccessActionResult(undefined, 'Successfully cleaned up old build jobs.');
|
||||
});
|
||||
|
||||
export const updateQuickstack = async () =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
|
||||
@@ -5,14 +5,11 @@ import PageTitle from "@/components/custom/page-title";
|
||||
import paramService, { ParamService } from "@/server/services/param.service";
|
||||
import QuickStackIngressSettings from "./qs-ingress-settings";
|
||||
import QuickStackLetsEncryptSettings from "./qs-letsencrypt-settings";
|
||||
import QuickStackMaintenanceSettings from "./qs-maintenance-settings";
|
||||
import podService from "@/server/services/pod.service";
|
||||
import { Constants } from "@/shared/utils/constants";
|
||||
import ServerBreadcrumbs from "./server-breadcrumbs";
|
||||
import QuickStackVersionInfo from "./qs-version-info";
|
||||
import QuickStackRegistrySettings from "./qs-registry-settings";
|
||||
import s3TargetService from "@/server/services/s3-target.service";
|
||||
import QuickStackPublicIpSettings from "./qs-public-ip-settings";
|
||||
import BreadcrumbSetter from "@/components/breadcrumbs-setter";
|
||||
|
||||
export default async function ProjectPage() {
|
||||
|
||||
@@ -21,10 +18,7 @@ export default async function ProjectPage() {
|
||||
const disableNodePortAccess = await paramService.getBoolean(ParamService.DISABLE_NODEPORT_ACCESS, false);
|
||||
const letsEncryptMail = await paramService.getString(ParamService.LETS_ENCRYPT_MAIL, session.email);
|
||||
const regitryStorageLocation = await paramService.getString(ParamService.REGISTRY_SOTRAGE_LOCATION, Constants.INTERNAL_REGISTRY_LOCATION);
|
||||
const useCanaryChannel = await paramService.getBoolean(ParamService.USE_CANARY_CHANNEL, false);
|
||||
const qsPodInfos = await podService.getPodsForApp(Constants.QS_NAMESPACE, Constants.QS_APP_NAME);
|
||||
const ipv4Address = await paramService.getString(ParamService.PUBLIC_IPV4_ADDRESS);
|
||||
const qsPodInfo = qsPodInfos.find(p => !!p);
|
||||
const s3Targets = await s3TargetService.getAll();
|
||||
|
||||
return (
|
||||
@@ -33,14 +27,15 @@ export default async function ProjectPage() {
|
||||
title={'Server Settings'}
|
||||
subtitle={`View or edit Server Settings`}>
|
||||
</PageTitle>
|
||||
<ServerBreadcrumbs />
|
||||
<BreadcrumbSetter items={[
|
||||
{ name: "Settings", url: "/settings/profile" },
|
||||
{ name: "QuickStack Server" },
|
||||
]} />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div><QuickStackIngressSettings disableNodePortAccess={disableNodePortAccess!} serverUrl={serverUrl!} /></div>
|
||||
<div> <QuickStackLetsEncryptSettings letsEncryptMail={letsEncryptMail!} /></div>
|
||||
<div> <QuickStackPublicIpSettings publicIpv4={ipv4Address} /></div>
|
||||
<div><QuickStackLetsEncryptSettings letsEncryptMail={letsEncryptMail!} /></div>
|
||||
<div><QuickStackPublicIpSettings publicIpv4={ipv4Address} /></div>
|
||||
<div><QuickStackRegistrySettings registryStorageLocation={regitryStorageLocation!} s3Targets={s3Targets} /></div>
|
||||
<div><QuickStackMaintenanceSettings qsPodName={qsPodInfo?.podName} /></div>
|
||||
<div><QuickStackVersionInfo useCanaryChannel={useCanaryChannel!} /></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useBreadcrumbs } from "@/frontend/states/zustand.states";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function ServerBreadcrumbs() {
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
useEffect(() => setBreadcrumbs([
|
||||
{ name: "Settings", url: "/settings/profile" },
|
||||
{ name: "QuickStack Server" },
|
||||
]), []);
|
||||
return <></>;
|
||||
}
|
||||
@@ -39,13 +39,13 @@ const settingsMenu = [
|
||||
icon: User,
|
||||
},
|
||||
{
|
||||
title: "QuickStack Settings",
|
||||
url: "/settings/server",
|
||||
title: "S3 Targets",
|
||||
url: "/settings/s3-targets",
|
||||
icon: Settings,
|
||||
},
|
||||
{
|
||||
title: "S3 Targets",
|
||||
url: "/settings/s3-targets",
|
||||
title: "QuickStack Settings",
|
||||
url: "/settings/server",
|
||||
icon: Settings,
|
||||
},
|
||||
{
|
||||
@@ -53,6 +53,11 @@ const settingsMenu = [
|
||||
url: "/settings/cluster",
|
||||
icon: Server,
|
||||
},
|
||||
{
|
||||
title: "Maintenance",
|
||||
url: "/settings/maintenance",
|
||||
icon: Settings,
|
||||
},
|
||||
]
|
||||
|
||||
export function SidebarCient({
|
||||
|
||||
15
src/components/breadcrumbs-setter.tsx
Normal file
15
src/components/breadcrumbs-setter.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useEffect } from "react";
|
||||
import { Breadcrumb, useBreadcrumbs } from "@/frontend/states/zustand.states";
|
||||
|
||||
export default function BreadcrumbSetter({ items }: { items: Breadcrumb[] }) {
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
useEffect(() => {
|
||||
setBreadcrumbs(items)
|
||||
return () => setBreadcrumbs([]);
|
||||
}, [items]);
|
||||
return <></>;
|
||||
}
|
||||
@@ -172,6 +172,17 @@ class BuildService {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAllFailedOrSuccededBuilds() {
|
||||
const jobs = await k3s.batch.listNamespacedJob(BUILD_NAMESPACE);
|
||||
const jobsToDelete = jobs.body.items.filter((job) => {
|
||||
const status = this.getJobStatusString(job.status);
|
||||
return status !== 'RUNNING';
|
||||
});
|
||||
for (const job of jobsToDelete) {
|
||||
await this.deleteBuild(job.metadata?.name!);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAllBuildsOfProject(projectId: string) {
|
||||
const jobs = await k3s.batch.listNamespacedJob(BUILD_NAMESPACE);
|
||||
const jobsOfProject = jobs.body.items.filter((job) => job.metadata?.annotations?.[Constants.QS_ANNOTATION_PROJECT_ID] === projectId);
|
||||
|
||||
@@ -9,6 +9,9 @@ import longhornApiAdapter from "../adapter/longhorn-api.adapter";
|
||||
import dataAccess from "../adapter/db.client";
|
||||
import pvcService from "./pvc.service";
|
||||
import { KubeObjectNameUtils } from "../utils/kube-object-name.utils";
|
||||
import appService from "./app.service";
|
||||
import projectService from "./project.service";
|
||||
import { AppMonitoringUsageModel } from "@/shared/model/app-monitoring-usage.model";
|
||||
|
||||
class MonitorService {
|
||||
|
||||
@@ -65,6 +68,43 @@ class MonitorService {
|
||||
return appVolumesWithUsage;
|
||||
}
|
||||
|
||||
async getMonitoringForAllApps() {
|
||||
const [topPods, totalResourcesNodes, projects] = await Promise.all([
|
||||
k8s.topPods(k3s.core, new k8s.Metrics(k3s.getKubeConfig())),
|
||||
this.getTotalAvailableNodeRessources(),
|
||||
projectService.getAllProjects()
|
||||
]);
|
||||
|
||||
const appStats: AppMonitoringUsageModel[] = [];
|
||||
|
||||
for (let project of projects) {
|
||||
for (let app of project.apps) {
|
||||
const podsFromApp = await standalonePodService.getPodsForApp(project.id, app.id);
|
||||
const filteredTopPods = topPods.filter((topPod) =>
|
||||
podsFromApp.some((pod) => pod.podName === topPod.Pod.metadata?.name)
|
||||
);
|
||||
const totalResourcesApp = this.calulateTotalRessourceUsageOfApp(filteredTopPods);
|
||||
const cpuUsagePercent = (totalResourcesApp.cpu / totalResourcesNodes.cpu) * 100;
|
||||
appStats.push({
|
||||
projectId: project.id,
|
||||
projectName: project.name,
|
||||
appName: app.name,
|
||||
appId: app.id,
|
||||
cpuUsage: totalResourcesApp.cpu,
|
||||
cpuUsagePercent,
|
||||
ramUsageBytes: totalResourcesApp.ramBytes
|
||||
})
|
||||
}
|
||||
}
|
||||
appStats.sort((a, b) => {
|
||||
if (a.projectName === b.projectName) {
|
||||
return a.appName.localeCompare(b.appName);
|
||||
}
|
||||
return a.projectName.localeCompare(b.projectName);
|
||||
});
|
||||
return appStats;
|
||||
}
|
||||
|
||||
async getMonitoringForApp(projectId: string, appId: string): Promise<PodsResourceInfoModel> {
|
||||
const metricsClient = new k8s.Metrics(k3s.getKubeConfig());
|
||||
const podsFromApp = await standalonePodService.getPodsForApp(projectId, appId);
|
||||
@@ -74,25 +114,8 @@ class MonitorService {
|
||||
podsFromApp.some((pod) => pod.podName === topPod.Pod.metadata?.name)
|
||||
);
|
||||
|
||||
const topNodes = await clusterService.getNodeInfo();
|
||||
const totalResourcesNodes = topNodes.reduce(
|
||||
(acc, node) => {
|
||||
acc.cpu += Number(node.cpuCapacity) || 0;
|
||||
acc.ramBytes += KubeSizeConverter.fromKubeSizeToBytes(node.ramCapacity) || 0;
|
||||
return acc;
|
||||
},
|
||||
{ cpu: 0, ramBytes: 0 }
|
||||
);
|
||||
|
||||
const totalResourcesApp = filteredTopPods.reduce(
|
||||
(acc, pod) => {
|
||||
acc.cpu += Number(pod.CPU.CurrentUsage) || 0;
|
||||
acc.ramBytes += Number(pod.Memory.CurrentUsage) || 0;
|
||||
return acc;
|
||||
},
|
||||
{ cpu: 0, ramBytes: 0 }
|
||||
);
|
||||
|
||||
const totalResourcesNodes = await this.getTotalAvailableNodeRessources();
|
||||
const totalResourcesApp = this.calulateTotalRessourceUsageOfApp(filteredTopPods);
|
||||
|
||||
var totalRamNodesCorrectUnit: number = totalResourcesNodes.ramBytes;
|
||||
var totalRamAppCorrectUnit: number = totalResourcesApp.ramBytes;
|
||||
@@ -108,6 +131,30 @@ class MonitorService {
|
||||
}
|
||||
}
|
||||
|
||||
private calulateTotalRessourceUsageOfApp(filteredTopPods: k8s.PodStatus[]) {
|
||||
return filteredTopPods.reduce(
|
||||
(acc, pod) => {
|
||||
acc.cpu += Number(pod.CPU.CurrentUsage) || 0;
|
||||
acc.ramBytes += Number(pod.Memory.CurrentUsage) || 0;
|
||||
return acc;
|
||||
},
|
||||
{ cpu: 0, ramBytes: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
private async getTotalAvailableNodeRessources() {
|
||||
const topNodes = await clusterService.getNodeInfo();
|
||||
const totalResourcesNodes = topNodes.reduce(
|
||||
(acc, node) => {
|
||||
acc.cpu += Number(node.cpuCapacity) || 0;
|
||||
acc.ramBytes += KubeSizeConverter.fromKubeSizeToBytes(node.ramCapacity) || 0;
|
||||
return acc;
|
||||
},
|
||||
{ cpu: 0, ramBytes: 0 }
|
||||
);
|
||||
return totalResourcesNodes;
|
||||
}
|
||||
|
||||
async getPvcUsageFromApp(appId: string, projectId: string): Promise<Array<{ pvcName: string, usedBytes: number }>> {
|
||||
const pvcFromApp = await pvcService.getAllPvcForApp(projectId, appId);
|
||||
const pvcUsageData: Array<{ pvcName: string, usedBytes: number }> = [];
|
||||
|
||||
9
src/shared/model/app-monitoring-usage.model.ts
Normal file
9
src/shared/model/app-monitoring-usage.model.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface AppMonitoringUsageModel {
|
||||
projectId: string,
|
||||
projectName: string,
|
||||
appName: string,
|
||||
appId: string,
|
||||
cpuUsage: number,
|
||||
cpuUsagePercent: number,
|
||||
ramUsageBytes: number
|
||||
}
|
||||
Reference in New Issue
Block a user