mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
import React, { FunctionComponent, ReactElement, useState } from "react";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import DictionaryOfStringsViewer from "Common/UI/Components/Dictionary/DictionaryOfStingsViewer";
|
||||
import {
|
||||
KubernetesContainerSpec,
|
||||
KubernetesContainerStatus,
|
||||
} from "../../Pages/Kubernetes/Utils/KubernetesObjectParser";
|
||||
|
||||
export interface ComponentProps {
|
||||
containers: Array<KubernetesContainerSpec>;
|
||||
initContainers: Array<KubernetesContainerSpec>;
|
||||
containerStatuses?: Array<KubernetesContainerStatus> | undefined;
|
||||
initContainerStatuses?: Array<KubernetesContainerStatus> | undefined;
|
||||
}
|
||||
|
||||
interface ContainerCardProps {
|
||||
container: KubernetesContainerSpec;
|
||||
status?: KubernetesContainerStatus | undefined;
|
||||
isInit: boolean;
|
||||
}
|
||||
|
||||
const ContainerCard: FunctionComponent<ContainerCardProps> = (
|
||||
props: ContainerCardProps,
|
||||
): ReactElement => {
|
||||
const [showEnv, setShowEnv] = useState<boolean>(false);
|
||||
const [showMounts, setShowMounts] = useState<boolean>(false);
|
||||
|
||||
const envRecord: Record<string, string> = {};
|
||||
for (const env of props.container.env) {
|
||||
envRecord[env.name] = env.value;
|
||||
}
|
||||
|
||||
const hasResources: boolean =
|
||||
Object.keys(props.container.resources.requests).length > 0 ||
|
||||
Object.keys(props.container.resources.limits).length > 0;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={`${props.isInit ? "Init Container: " : "Container: "}${props.container.name}`}
|
||||
description={props.container.image}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Status */}
|
||||
{props.status && (
|
||||
<div className="flex gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">State:</span>{" "}
|
||||
<span
|
||||
className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
|
||||
props.status.state === "running"
|
||||
? "bg-green-50 text-green-700"
|
||||
: props.status.state === "waiting"
|
||||
? "bg-yellow-50 text-yellow-700"
|
||||
: "bg-red-50 text-red-700"
|
||||
}`}
|
||||
>
|
||||
{props.status.state}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">Ready:</span>{" "}
|
||||
<span
|
||||
className={
|
||||
props.status.ready ? "text-green-700" : "text-red-700"
|
||||
}
|
||||
>
|
||||
{props.status.ready ? "Yes" : "No"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">Restarts:</span>{" "}
|
||||
<span
|
||||
className={
|
||||
props.status.restartCount > 0
|
||||
? "text-yellow-700"
|
||||
: "text-gray-700"
|
||||
}
|
||||
>
|
||||
{props.status.restartCount}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Command & Args */}
|
||||
{props.container.command.length > 0 && (
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-500 font-medium">Command:</span>{" "}
|
||||
<code className="text-xs bg-gray-100 px-1 py-0.5 rounded">
|
||||
{props.container.command.join(" ")}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
{props.container.args.length > 0 && (
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-500 font-medium">Args:</span>{" "}
|
||||
<code className="text-xs bg-gray-100 px-1 py-0.5 rounded">
|
||||
{props.container.args.join(" ")}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ports */}
|
||||
{props.container.ports.length > 0 && (
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-500 font-medium">Ports:</span>{" "}
|
||||
{props.container.ports.map((port, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="inline-flex px-2 py-0.5 text-xs font-medium rounded bg-blue-50 text-blue-700 mr-1"
|
||||
>
|
||||
{port.name ? `${port.name}: ` : ""}
|
||||
{port.containerPort}/{port.protocol}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Resources */}
|
||||
{hasResources && (
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
{Object.keys(props.container.resources.requests).length > 0 && (
|
||||
<div>
|
||||
<span className="text-gray-500 font-medium block mb-1">
|
||||
Requests:
|
||||
</span>
|
||||
<DictionaryOfStringsViewer
|
||||
value={props.container.resources.requests}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{Object.keys(props.container.resources.limits).length > 0 && (
|
||||
<div>
|
||||
<span className="text-gray-500 font-medium block mb-1">
|
||||
Limits:
|
||||
</span>
|
||||
<DictionaryOfStringsViewer
|
||||
value={props.container.resources.limits}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Environment Variables (expandable) */}
|
||||
{props.container.env.length > 0 && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowEnv(!showEnv);
|
||||
}}
|
||||
className="text-sm text-indigo-600 hover:text-indigo-900 font-medium"
|
||||
>
|
||||
{showEnv ? "Hide" : "Show"} Environment Variables (
|
||||
{props.container.env.length})
|
||||
</button>
|
||||
{showEnv && (
|
||||
<div className="mt-2">
|
||||
<DictionaryOfStringsViewer value={envRecord} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Volume Mounts (expandable) */}
|
||||
{props.container.volumeMounts.length > 0 && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowMounts(!showMounts);
|
||||
}}
|
||||
className="text-sm text-indigo-600 hover:text-indigo-900 font-medium"
|
||||
>
|
||||
{showMounts ? "Hide" : "Show"} Volume Mounts (
|
||||
{props.container.volumeMounts.length})
|
||||
</button>
|
||||
{showMounts && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{props.container.volumeMounts.map((mount, idx) => (
|
||||
<div key={idx} className="text-sm flex gap-2">
|
||||
<span className="font-medium text-gray-700">
|
||||
{mount.name}
|
||||
</span>
|
||||
<span className="text-gray-500">→</span>
|
||||
<code className="text-xs bg-gray-100 px-1 py-0.5 rounded">
|
||||
{mount.mountPath}
|
||||
</code>
|
||||
{mount.readOnly && (
|
||||
<span className="text-xs text-gray-400">(read-only)</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const KubernetesContainersTab: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
if (
|
||||
props.containers.length === 0 &&
|
||||
props.initContainers.length === 0
|
||||
) {
|
||||
return (
|
||||
<div className="text-gray-500 text-sm p-4">
|
||||
No container information available.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStatus: (
|
||||
name: string,
|
||||
isInit: boolean,
|
||||
) => KubernetesContainerStatus | undefined = (
|
||||
name: string,
|
||||
isInit: boolean,
|
||||
): KubernetesContainerStatus | undefined => {
|
||||
const statuses: Array<KubernetesContainerStatus> | undefined = isInit
|
||||
? props.initContainerStatuses
|
||||
: props.containerStatuses;
|
||||
return statuses?.find(
|
||||
(s: KubernetesContainerStatus) => s.name === name,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{props.initContainers.map(
|
||||
(container: KubernetesContainerSpec, index: number) => (
|
||||
<ContainerCard
|
||||
key={`init-${index}`}
|
||||
container={container}
|
||||
status={getStatus(container.name, true)}
|
||||
isInit={true}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{props.containers.map(
|
||||
(container: KubernetesContainerSpec, index: number) => (
|
||||
<ContainerCard
|
||||
key={`container-${index}`}
|
||||
container={container}
|
||||
status={getStatus(container.name, false)}
|
||||
isInit={false}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesContainersTab;
|
||||
@@ -0,0 +1,127 @@
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
fetchK8sEventsForResource,
|
||||
KubernetesEvent,
|
||||
} from "../../Pages/Kubernetes/Utils/KubernetesObjectFetcher";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
|
||||
export interface ComponentProps {
|
||||
clusterIdentifier: string;
|
||||
resourceKind: string; // "Pod", "Node", "Deployment", etc.
|
||||
resourceName: string;
|
||||
namespace?: string | undefined;
|
||||
}
|
||||
|
||||
const KubernetesEventsTab: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const [events, setEvents] = useState<Array<KubernetesEvent>>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEvents: () => Promise<void> = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result: Array<KubernetesEvent> =
|
||||
await fetchK8sEventsForResource({
|
||||
clusterIdentifier: props.clusterIdentifier,
|
||||
resourceKind: props.resourceKind,
|
||||
resourceName: props.resourceName,
|
||||
namespace: props.namespace,
|
||||
});
|
||||
setEvents(result);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to fetch events",
|
||||
);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchEvents().catch(() => {});
|
||||
}, [
|
||||
props.clusterIdentifier,
|
||||
props.resourceKind,
|
||||
props.resourceName,
|
||||
props.namespace,
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (events.length === 0) {
|
||||
return (
|
||||
<div className="text-gray-500 text-sm p-4">
|
||||
No events found for this {props.resourceKind.toLowerCase()} in the last
|
||||
24 hours.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Time
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Type
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Reason
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Message
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{events.map((event: KubernetesEvent, index: number) => {
|
||||
const isWarning: boolean =
|
||||
event.type.toLowerCase() === "warning";
|
||||
return (
|
||||
<tr key={index} className={isWarning ? "bg-yellow-50" : ""}>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
||||
{event.timestamp}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
||||
<span
|
||||
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
isWarning
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: "bg-green-100 text-green-800"
|
||||
}`}
|
||||
>
|
||||
{event.type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
||||
{event.reason}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-500 max-w-lg">
|
||||
{event.message}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesEventsTab;
|
||||
@@ -0,0 +1,118 @@
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
fetchPodLogs,
|
||||
KubernetesLogEntry,
|
||||
} from "../../Pages/Kubernetes/Utils/KubernetesObjectFetcher";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
|
||||
export interface ComponentProps {
|
||||
clusterIdentifier: string;
|
||||
podName: string;
|
||||
containerName?: string | undefined;
|
||||
namespace?: string | undefined;
|
||||
}
|
||||
|
||||
const KubernetesLogsTab: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const [logs, setLogs] = useState<Array<KubernetesLogEntry>>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLogs: () => Promise<void> = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result: Array<KubernetesLogEntry> = await fetchPodLogs({
|
||||
clusterIdentifier: props.clusterIdentifier,
|
||||
podName: props.podName,
|
||||
containerName: props.containerName,
|
||||
namespace: props.namespace,
|
||||
limit: 500,
|
||||
});
|
||||
setLogs(result);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to fetch logs",
|
||||
);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchLogs().catch(() => {});
|
||||
}, [
|
||||
props.clusterIdentifier,
|
||||
props.podName,
|
||||
props.containerName,
|
||||
props.namespace,
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (logs.length === 0) {
|
||||
return (
|
||||
<div className="text-gray-500 text-sm p-4">
|
||||
No application logs found for this pod in the last 6 hours. Logs will
|
||||
appear here once the kubernetes-agent's filelog receiver is collecting
|
||||
data.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getSeverityColor: (severity: string) => string = (
|
||||
severity: string,
|
||||
): string => {
|
||||
const s: string = severity.toUpperCase();
|
||||
if (s === "ERROR" || s === "FATAL" || s === "CRITICAL") {
|
||||
return "text-red-600";
|
||||
}
|
||||
if (s === "WARN" || s === "WARNING") {
|
||||
return "text-yellow-600";
|
||||
}
|
||||
if (s === "DEBUG" || s === "TRACE") {
|
||||
return "text-gray-400";
|
||||
}
|
||||
return "text-gray-700";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 rounded-lg p-4 overflow-auto max-h-[600px] font-mono text-xs">
|
||||
{logs.map((log: KubernetesLogEntry, index: number) => {
|
||||
return (
|
||||
<div key={index} className="flex gap-2 py-0.5 hover:bg-gray-800">
|
||||
<span className="text-gray-500 whitespace-nowrap flex-shrink-0">
|
||||
{log.timestamp}
|
||||
</span>
|
||||
{log.containerName && (
|
||||
<span className="text-blue-400 whitespace-nowrap flex-shrink-0">
|
||||
[{log.containerName}]
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`whitespace-nowrap flex-shrink-0 w-12 ${getSeverityColor(log.severity)}`}
|
||||
>
|
||||
{log.severity}
|
||||
</span>
|
||||
<span className="text-gray-200 whitespace-pre-wrap break-all">
|
||||
{log.body}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesLogsTab;
|
||||
@@ -0,0 +1,47 @@
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useState,
|
||||
} from "react";
|
||||
import MetricView from "../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
|
||||
export interface ComponentProps {
|
||||
queryConfigs: Array<MetricQueryConfigData>;
|
||||
}
|
||||
|
||||
const KubernetesMetricsTab: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
return (
|
||||
<MetricView
|
||||
data={{
|
||||
...metricViewData,
|
||||
queryConfigs: props.queryConfigs,
|
||||
}}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: props.queryConfigs,
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesMetricsTab;
|
||||
@@ -0,0 +1,168 @@
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import DictionaryOfStringsViewer from "Common/UI/Components/Dictionary/DictionaryOfStingsViewer";
|
||||
import { KubernetesCondition } from "../../Pages/Kubernetes/Utils/KubernetesObjectParser";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
|
||||
export interface SummaryField {
|
||||
title: string;
|
||||
value: string | ReactElement;
|
||||
}
|
||||
|
||||
export interface ComponentProps {
|
||||
summaryFields: Array<SummaryField>;
|
||||
labels: Record<string, string>;
|
||||
annotations: Record<string, string>;
|
||||
conditions?: Array<KubernetesCondition> | undefined;
|
||||
ownerReferences?: Array<{ kind: string; name: string }> | undefined;
|
||||
isLoading: boolean;
|
||||
emptyMessage?: string | undefined;
|
||||
}
|
||||
|
||||
const KubernetesOverviewTab: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
if (props.isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (
|
||||
props.summaryFields.length === 0 &&
|
||||
Object.keys(props.labels).length === 0
|
||||
) {
|
||||
return (
|
||||
<div className="text-gray-500 text-sm p-4">
|
||||
{props.emptyMessage ||
|
||||
"Resource details not yet available. Ensure the kubernetes-agent Helm chart has resourceSpecs.enabled set to true and wait for the next data pull (up to 5 minutes)."}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Summary Info Cards */}
|
||||
{props.summaryFields.length > 0 && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{props.summaryFields.map(
|
||||
(field: SummaryField, index: number) => {
|
||||
return (
|
||||
<InfoCard
|
||||
key={index}
|
||||
title={field.title}
|
||||
value={field.value}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Owner References */}
|
||||
{props.ownerReferences && props.ownerReferences.length > 0 && (
|
||||
<Card title="Owner References" description="Resources that own this object.">
|
||||
<div className="space-y-1">
|
||||
{props.ownerReferences.map(
|
||||
(ref: { kind: string; name: string }, index: number) => {
|
||||
return (
|
||||
<div key={index} className="text-sm">
|
||||
<span className="font-medium text-gray-700">
|
||||
{ref.kind}:
|
||||
</span>{" "}
|
||||
<span className="text-gray-600">{ref.name}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Conditions */}
|
||||
{props.conditions && props.conditions.length > 0 && (
|
||||
<Card
|
||||
title="Conditions"
|
||||
description="Current status conditions of this resource."
|
||||
>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Type
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Reason
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Message
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Last Transition
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{props.conditions.map(
|
||||
(condition: KubernetesCondition, index: number) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{condition.type}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
||||
<span
|
||||
className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
|
||||
condition.status === "True"
|
||||
? "bg-green-50 text-green-700"
|
||||
: condition.status === "False"
|
||||
? "bg-red-50 text-red-700"
|
||||
: "bg-gray-50 text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{condition.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">
|
||||
{condition.reason || "-"}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600 max-w-md truncate">
|
||||
{condition.message || "-"}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
||||
{condition.lastTransitionTime || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Labels */}
|
||||
{Object.keys(props.labels).length > 0 && (
|
||||
<Card title="Labels" description="Key-value labels attached to this resource.">
|
||||
<DictionaryOfStringsViewer value={props.labels} />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Annotations */}
|
||||
{Object.keys(props.annotations).length > 0 && (
|
||||
<Card
|
||||
title="Annotations"
|
||||
description="Metadata annotations on this resource."
|
||||
>
|
||||
<DictionaryOfStringsViewer value={props.annotations} />
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesOverviewTab;
|
||||
@@ -0,0 +1,410 @@
|
||||
import Log from "Common/Models/AnalyticsModels/Log";
|
||||
import AnalyticsModelAPI, {
|
||||
ListResult,
|
||||
} from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import {
|
||||
extractObjectFromLogBody,
|
||||
getKvStringValue,
|
||||
getKvValue,
|
||||
KubernetesPodObject,
|
||||
KubernetesNodeObject,
|
||||
KubernetesDeploymentObject,
|
||||
KubernetesStatefulSetObject,
|
||||
KubernetesDaemonSetObject,
|
||||
KubernetesJobObject,
|
||||
KubernetesCronJobObject,
|
||||
KubernetesNamespaceObject,
|
||||
parsePodObject,
|
||||
parseNodeObject,
|
||||
parseDeploymentObject,
|
||||
parseStatefulSetObject,
|
||||
parseDaemonSetObject,
|
||||
parseJobObject,
|
||||
parseCronJobObject,
|
||||
parseNamespaceObject,
|
||||
} from "./KubernetesObjectParser";
|
||||
|
||||
export type KubernetesObjectType =
|
||||
| KubernetesPodObject
|
||||
| KubernetesNodeObject
|
||||
| KubernetesDeploymentObject
|
||||
| KubernetesStatefulSetObject
|
||||
| KubernetesDaemonSetObject
|
||||
| KubernetesJobObject
|
||||
| KubernetesCronJobObject
|
||||
| KubernetesNamespaceObject;
|
||||
|
||||
export interface FetchK8sObjectOptions {
|
||||
clusterIdentifier: string;
|
||||
resourceType: string; // "pods", "nodes", "deployments", etc.
|
||||
resourceName: string;
|
||||
namespace?: string | undefined; // Not needed for cluster-scoped resources (nodes, namespaces)
|
||||
}
|
||||
|
||||
type ParserFunction = (kvList: JSONObject) => KubernetesObjectType | null;
|
||||
|
||||
function getParser(resourceType: string): ParserFunction | null {
|
||||
const parsers: Record<string, ParserFunction> = {
|
||||
pods: parsePodObject,
|
||||
nodes: parseNodeObject,
|
||||
deployments: parseDeploymentObject,
|
||||
statefulsets: parseStatefulSetObject,
|
||||
daemonsets: parseDaemonSetObject,
|
||||
jobs: parseJobObject,
|
||||
cronjobs: parseCronJobObject,
|
||||
namespaces: parseNamespaceObject,
|
||||
};
|
||||
return parsers[resourceType] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest K8s resource object from the Log table.
|
||||
* The k8sobjects pull mode stores full K8s API objects as log entries.
|
||||
*/
|
||||
export async function fetchLatestK8sObject<T extends KubernetesObjectType>(
|
||||
options: FetchK8sObjectOptions,
|
||||
): Promise<T | null> {
|
||||
const parser: ParserFunction | null = getParser(options.resourceType);
|
||||
if (!parser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const projectId: string | undefined =
|
||||
ProjectUtil.getCurrentProjectId()?.toString();
|
||||
if (!projectId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -24);
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const queryOptions: any = {
|
||||
modelType: Log,
|
||||
query: {
|
||||
projectId: projectId,
|
||||
time: new InBetween<Date>(startDate, endDate),
|
||||
attributes: {
|
||||
"logAttributes.event.domain": "k8s",
|
||||
"logAttributes.k8s.resource.name": options.resourceType,
|
||||
},
|
||||
},
|
||||
limit: 500, // Get enough logs to find the resource
|
||||
skip: 0,
|
||||
select: {
|
||||
time: true,
|
||||
body: true,
|
||||
attributes: true,
|
||||
},
|
||||
sort: {
|
||||
time: SortOrder.Descending,
|
||||
},
|
||||
requestOptions: {},
|
||||
};
|
||||
const listResult: ListResult<Log> =
|
||||
await AnalyticsModelAPI.getList<Log>(queryOptions);
|
||||
|
||||
// Parse each log body and find the matching resource
|
||||
for (const log of listResult.data) {
|
||||
const attrs: JSONObject = log.attributes || {};
|
||||
|
||||
// Filter to this cluster
|
||||
if (
|
||||
attrs["resource.k8s.cluster.name"] !== options.clusterIdentifier &&
|
||||
attrs["k8s.cluster.name"] !== options.clusterIdentifier
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof log.body !== "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectKvList: JSONObject | null = extractObjectFromLogBody(
|
||||
log.body,
|
||||
);
|
||||
if (!objectKvList) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is the resource we're looking for
|
||||
const metadataKv: string | JSONObject | null = getKvValue(
|
||||
objectKvList,
|
||||
"metadata",
|
||||
);
|
||||
if (!metadataKv || typeof metadataKv === "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name: string = getKvStringValue(metadataKv, "name");
|
||||
const namespace: string = getKvStringValue(metadataKv, "namespace");
|
||||
|
||||
if (name !== options.resourceName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For namespaced resources, also match namespace
|
||||
if (options.namespace && namespace && namespace !== options.namespace) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsed: KubernetesObjectType | null = parser(objectKvList);
|
||||
if (parsed) {
|
||||
return parsed as T;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch K8s events related to a specific resource.
|
||||
*/
|
||||
export interface KubernetesEvent {
|
||||
timestamp: string;
|
||||
type: string;
|
||||
reason: string;
|
||||
objectKind: string;
|
||||
objectName: string;
|
||||
namespace: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export async function fetchK8sEventsForResource(options: {
|
||||
clusterIdentifier: string;
|
||||
resourceKind: string; // "Pod", "Node", "Deployment", etc.
|
||||
resourceName: string;
|
||||
namespace?: string | undefined;
|
||||
}): Promise<Array<KubernetesEvent>> {
|
||||
const projectId: string | undefined =
|
||||
ProjectUtil.getCurrentProjectId()?.toString();
|
||||
if (!projectId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -24);
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const eventsQueryOptions: any = {
|
||||
modelType: Log,
|
||||
query: {
|
||||
projectId: projectId,
|
||||
time: new InBetween<Date>(startDate, endDate),
|
||||
attributes: {
|
||||
"logAttributes.event.domain": "k8s",
|
||||
"logAttributes.k8s.resource.name": "events",
|
||||
},
|
||||
},
|
||||
limit: 500,
|
||||
skip: 0,
|
||||
select: {
|
||||
time: true,
|
||||
body: true,
|
||||
attributes: true,
|
||||
},
|
||||
sort: {
|
||||
time: SortOrder.Descending,
|
||||
},
|
||||
requestOptions: {},
|
||||
};
|
||||
const listResult: ListResult<Log> =
|
||||
await AnalyticsModelAPI.getList<Log>(eventsQueryOptions);
|
||||
|
||||
const events: Array<KubernetesEvent> = [];
|
||||
|
||||
for (const log of listResult.data) {
|
||||
const attrs: JSONObject = log.attributes || {};
|
||||
|
||||
// Filter to this cluster
|
||||
if (
|
||||
attrs["resource.k8s.cluster.name"] !== options.clusterIdentifier &&
|
||||
attrs["k8s.cluster.name"] !== options.clusterIdentifier
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof log.body !== "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let bodyObj: JSONObject | null = null;
|
||||
try {
|
||||
bodyObj = JSON.parse(log.body) as JSONObject;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const topKvList: JSONObject | undefined = bodyObj["kvlistValue"] as
|
||||
| JSONObject
|
||||
| undefined;
|
||||
if (!topKvList) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the "object" which is the actual k8s Event
|
||||
const objectVal: string | JSONObject | null = getKvValue(
|
||||
topKvList,
|
||||
"object",
|
||||
);
|
||||
if (!objectVal || typeof objectVal === "string") {
|
||||
continue;
|
||||
}
|
||||
const objectKvList: JSONObject = objectVal;
|
||||
|
||||
// Get event details
|
||||
const eventType: string =
|
||||
getKvStringValue(objectKvList, "type") || "";
|
||||
const reason: string = getKvStringValue(objectKvList, "reason") || "";
|
||||
const note: string = getKvStringValue(objectKvList, "note") || "";
|
||||
|
||||
// Get regarding object
|
||||
const regardingKind: string =
|
||||
getKvStringValue(
|
||||
getKvValue(objectKvList, "regarding") as JSONObject | undefined,
|
||||
"kind",
|
||||
) || "";
|
||||
const regardingName: string =
|
||||
getKvStringValue(
|
||||
getKvValue(objectKvList, "regarding") as JSONObject | undefined,
|
||||
"name",
|
||||
) || "";
|
||||
const regardingNamespace: string =
|
||||
getKvStringValue(
|
||||
getKvValue(objectKvList, "regarding") as JSONObject | undefined,
|
||||
"namespace",
|
||||
) || "";
|
||||
|
||||
// Filter to events for this specific resource
|
||||
if (
|
||||
regardingKind.toLowerCase() !== options.resourceKind.toLowerCase() ||
|
||||
regardingName !== options.resourceName
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
options.namespace &&
|
||||
regardingNamespace &&
|
||||
regardingNamespace !== options.namespace
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
events.push({
|
||||
timestamp: log.time
|
||||
? OneUptimeDate.getDateAsLocalFormattedString(log.time)
|
||||
: "",
|
||||
type: eventType || "Unknown",
|
||||
reason: reason || "Unknown",
|
||||
objectKind: regardingKind || "Unknown",
|
||||
objectName: regardingName || "Unknown",
|
||||
namespace: regardingNamespace || "default",
|
||||
message: note || "",
|
||||
});
|
||||
}
|
||||
|
||||
return events;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch application logs for a pod/container from the Log table.
|
||||
* These come from the filelog receiver (not k8sobjects).
|
||||
*/
|
||||
export interface KubernetesLogEntry {
|
||||
timestamp: string;
|
||||
body: string;
|
||||
severity: string;
|
||||
containerName: string;
|
||||
}
|
||||
|
||||
export async function fetchPodLogs(options: {
|
||||
clusterIdentifier: string;
|
||||
podName: string;
|
||||
containerName?: string | undefined;
|
||||
namespace?: string | undefined;
|
||||
limit?: number | undefined;
|
||||
}): Promise<Array<KubernetesLogEntry>> {
|
||||
const projectId: string | undefined =
|
||||
ProjectUtil.getCurrentProjectId()?.toString();
|
||||
if (!projectId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
|
||||
// Build attribute filters for filelog data
|
||||
const attributeFilters: Record<string, string> = {
|
||||
"resource.k8s.cluster.name": options.clusterIdentifier,
|
||||
"resource.k8s.pod.name": options.podName,
|
||||
};
|
||||
|
||||
if (options.containerName) {
|
||||
attributeFilters["resource.k8s.container.name"] = options.containerName;
|
||||
}
|
||||
|
||||
if (options.namespace) {
|
||||
attributeFilters["resource.k8s.namespace.name"] = options.namespace;
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const logsQueryOptions: any = {
|
||||
modelType: Log,
|
||||
query: {
|
||||
projectId: projectId,
|
||||
time: new InBetween<Date>(startDate, endDate),
|
||||
attributes: attributeFilters,
|
||||
},
|
||||
limit: options.limit || 200,
|
||||
skip: 0,
|
||||
select: {
|
||||
time: true,
|
||||
body: true,
|
||||
severityText: true,
|
||||
attributes: true,
|
||||
},
|
||||
sort: {
|
||||
time: SortOrder.Descending,
|
||||
},
|
||||
requestOptions: {},
|
||||
};
|
||||
const listResult: ListResult<Log> =
|
||||
await AnalyticsModelAPI.getList<Log>(logsQueryOptions);
|
||||
|
||||
return listResult.data
|
||||
.filter((log: Log) => {
|
||||
// Exclude k8s event logs — only application logs
|
||||
const attrs: JSONObject = log.attributes || {};
|
||||
return attrs["logAttributes.event.domain"] !== "k8s";
|
||||
})
|
||||
.map((log: Log) => {
|
||||
const attrs: JSONObject = log.attributes || {};
|
||||
return {
|
||||
timestamp: log.time
|
||||
? OneUptimeDate.getDateAsLocalFormattedString(log.time)
|
||||
: "",
|
||||
body: typeof log.body === "string" ? log.body : "",
|
||||
severity: log.severityText || "INFO",
|
||||
containerName:
|
||||
(attrs["resource.k8s.container.name"] as string) || "",
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData, {
|
||||
ChartSeries,
|
||||
} from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
@@ -25,6 +21,10 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
import Tabs from "Common/UI/Components/Tabs/Tabs";
|
||||
import { Tab } from "Common/UI/Components/Tabs/Tab";
|
||||
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
|
||||
import KubernetesLogsTab from "../../../Components/Kubernetes/KubernetesLogsTab";
|
||||
|
||||
const KubernetesClusterContainerDetail: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -36,16 +36,6 @@ const KubernetesClusterContainerDetail: FunctionComponent<
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -89,7 +79,8 @@ const KubernetesClusterContainerDetail: FunctionComponent<
|
||||
const attributes: Record<string, unknown> =
|
||||
(data["attributes"] as Record<string, unknown>) || {};
|
||||
const name: string =
|
||||
(attributes["resource.k8s.container.name"] as string) || "Unknown Container";
|
||||
(attributes["resource.k8s.container.name"] as string) ||
|
||||
"Unknown Container";
|
||||
return { title: name };
|
||||
};
|
||||
|
||||
@@ -143,32 +134,55 @@ const KubernetesClusterContainerDetail: FunctionComponent<
|
||||
getSeries: getSeries,
|
||||
};
|
||||
|
||||
const tabs: Array<Tab> = [
|
||||
{
|
||||
name: "Overview",
|
||||
children: (
|
||||
<div className="p-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<InfoCard title="Container Name" value={containerName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Logs",
|
||||
children: (
|
||||
<Card title="Container Logs" description="Logs for this container from the last 6 hours.">
|
||||
<KubernetesLogsTab
|
||||
clusterIdentifier={clusterIdentifier}
|
||||
podName=""
|
||||
containerName={containerName}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Metrics",
|
||||
children: (
|
||||
<Card
|
||||
title={`Container Metrics: ${containerName}`}
|
||||
description="CPU and memory usage for this container over the last 6 hours."
|
||||
>
|
||||
<KubernetesMetricsTab
|
||||
queryConfigs={[cpuQuery, memoryQuery]}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Container" value={containerName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
<div className="mb-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Container" value={containerName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title={`Container Metrics: ${containerName}`}
|
||||
description="CPU and memory usage for this container over the last 6 hours."
|
||||
>
|
||||
<MetricView
|
||||
data={{
|
||||
...metricViewData,
|
||||
queryConfigs: [cpuQuery, memoryQuery],
|
||||
}}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: [cpuQuery, memoryQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<Tabs tabs={tabs} onTabChange={() => {}} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData, {
|
||||
ChartSeries,
|
||||
} from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
@@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
import Tabs from "Common/UI/Components/Tabs/Tabs";
|
||||
import { Tab } from "Common/UI/Components/Tabs/Tab";
|
||||
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
|
||||
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
|
||||
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
|
||||
import { KubernetesCronJobObject } from "../Utils/KubernetesObjectParser";
|
||||
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
|
||||
|
||||
const KubernetesClusterCronJobDetail: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -35,16 +38,9 @@ const KubernetesClusterCronJobDetail: FunctionComponent<
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
const [cronJobObject, setCronJobObject] =
|
||||
useState<KubernetesCronJobObject | null>(null);
|
||||
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
@@ -69,6 +65,32 @@ const KubernetesClusterCronJobDetail: FunctionComponent<
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Fetch the K8s cronjob object for overview tab
|
||||
useEffect(() => {
|
||||
if (!cluster?.clusterIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchCronJobObject: () => Promise<void> =
|
||||
async (): Promise<void> => {
|
||||
setIsLoadingObject(true);
|
||||
try {
|
||||
const obj: KubernetesCronJobObject | null =
|
||||
await fetchLatestK8sObject<KubernetesCronJobObject>({
|
||||
clusterIdentifier: cluster.clusterIdentifier || "",
|
||||
resourceType: "cronjobs",
|
||||
resourceName: cronJobName,
|
||||
});
|
||||
setCronJobObject(obj);
|
||||
} catch {
|
||||
// Graceful degradation — overview tab shows empty state
|
||||
}
|
||||
setIsLoadingObject(false);
|
||||
};
|
||||
|
||||
fetchCronJobObject().catch(() => {});
|
||||
}, [cluster?.clusterIdentifier, cronJobName]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
@@ -143,32 +165,104 @@ const KubernetesClusterCronJobDetail: FunctionComponent<
|
||||
getSeries: getSeries,
|
||||
};
|
||||
|
||||
// Build overview summary fields from cronjob object
|
||||
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
|
||||
[
|
||||
{ title: "CronJob Name", value: cronJobName },
|
||||
{ title: "Cluster", value: clusterIdentifier },
|
||||
];
|
||||
|
||||
if (cronJobObject) {
|
||||
summaryFields.push(
|
||||
{
|
||||
title: "Namespace",
|
||||
value: cronJobObject.metadata.namespace || "default",
|
||||
},
|
||||
{
|
||||
title: "Schedule",
|
||||
value: cronJobObject.spec.schedule || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Suspend",
|
||||
value: cronJobObject.spec.suspend ? "Yes" : "No",
|
||||
},
|
||||
{
|
||||
title: "Concurrency Policy",
|
||||
value: cronJobObject.spec.concurrencyPolicy || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Successful Jobs History Limit",
|
||||
value: String(cronJobObject.spec.successfulJobsHistoryLimit ?? "N/A"),
|
||||
},
|
||||
{
|
||||
title: "Failed Jobs History Limit",
|
||||
value: String(cronJobObject.spec.failedJobsHistoryLimit ?? "N/A"),
|
||||
},
|
||||
{
|
||||
title: "Last Schedule Time",
|
||||
value: cronJobObject.status.lastScheduleTime || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Active Jobs",
|
||||
value: String(cronJobObject.status.activeCount ?? 0),
|
||||
},
|
||||
{
|
||||
title: "Created",
|
||||
value: cronJobObject.metadata.creationTimestamp || "N/A",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const tabs: Array<Tab> = [
|
||||
{
|
||||
name: "Overview",
|
||||
children: (
|
||||
<KubernetesOverviewTab
|
||||
summaryFields={summaryFields}
|
||||
labels={cronJobObject?.metadata.labels || {}}
|
||||
annotations={cronJobObject?.metadata.annotations || {}}
|
||||
isLoading={isLoadingObject}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Events",
|
||||
children: (
|
||||
<Card title="CronJob Events" description="Kubernetes events for this cronjob in the last 24 hours.">
|
||||
<KubernetesEventsTab
|
||||
clusterIdentifier={clusterIdentifier}
|
||||
resourceKind="CronJob"
|
||||
resourceName={cronJobName}
|
||||
namespace={cronJobObject?.metadata.namespace}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Metrics",
|
||||
children: (
|
||||
<Card
|
||||
title={`CronJob Metrics: ${cronJobName}`}
|
||||
description="CPU and memory usage for pods in this cronjob over the last 6 hours."
|
||||
>
|
||||
<KubernetesMetricsTab
|
||||
queryConfigs={[cpuQuery, memoryQuery]}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="CronJob" value={cronJobName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
<div className="mb-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="CronJob Name" value={cronJobName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title={`CronJob Metrics: ${cronJobName}`}
|
||||
description="CPU and memory usage for pods in this cronjob over the last 6 hours."
|
||||
>
|
||||
<MetricView
|
||||
data={{
|
||||
...metricViewData,
|
||||
queryConfigs: [cpuQuery, memoryQuery],
|
||||
}}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: [cpuQuery, memoryQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<Tabs tabs={tabs} onTabChange={() => {}} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData, {
|
||||
ChartSeries,
|
||||
} from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
@@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
import Tabs from "Common/UI/Components/Tabs/Tabs";
|
||||
import { Tab } from "Common/UI/Components/Tabs/Tab";
|
||||
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
|
||||
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
|
||||
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
|
||||
import { KubernetesDaemonSetObject } from "../Utils/KubernetesObjectParser";
|
||||
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
|
||||
|
||||
const KubernetesClusterDaemonSetDetail: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -35,16 +38,9 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent<
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
const [objectData, setObjectData] =
|
||||
useState<KubernetesDaemonSetObject | null>(null);
|
||||
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
@@ -69,6 +65,31 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent<
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Fetch the K8s daemonset object for overview tab
|
||||
useEffect(() => {
|
||||
if (!cluster?.clusterIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchObject: () => Promise<void> = async (): Promise<void> => {
|
||||
setIsLoadingObject(true);
|
||||
try {
|
||||
const obj: KubernetesDaemonSetObject | null =
|
||||
await fetchLatestK8sObject<KubernetesDaemonSetObject>({
|
||||
clusterIdentifier: cluster.clusterIdentifier || "",
|
||||
resourceType: "daemonsets",
|
||||
resourceName: daemonSetName,
|
||||
});
|
||||
setObjectData(obj);
|
||||
} catch {
|
||||
// Graceful degradation — overview tab shows empty state
|
||||
}
|
||||
setIsLoadingObject(false);
|
||||
};
|
||||
|
||||
fetchObject().catch(() => {});
|
||||
}, [cluster?.clusterIdentifier, daemonSetName]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
@@ -143,32 +164,99 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent<
|
||||
getSeries: getSeries,
|
||||
};
|
||||
|
||||
// Build overview summary fields from daemonset object
|
||||
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
|
||||
[
|
||||
{ title: "Name", value: daemonSetName },
|
||||
{ title: "Cluster", value: clusterIdentifier },
|
||||
];
|
||||
|
||||
if (objectData) {
|
||||
summaryFields.push(
|
||||
{
|
||||
title: "Namespace",
|
||||
value: objectData.metadata.namespace || "default",
|
||||
},
|
||||
{
|
||||
title: "Desired Scheduled",
|
||||
value: String(objectData.status.desiredNumberScheduled ?? "N/A"),
|
||||
},
|
||||
{
|
||||
title: "Current Scheduled",
|
||||
value: String(objectData.status.currentNumberScheduled ?? "N/A"),
|
||||
},
|
||||
{
|
||||
title: "Number Ready",
|
||||
value: String(objectData.status.numberReady ?? "N/A"),
|
||||
},
|
||||
{
|
||||
title: "Number Available",
|
||||
value: String(objectData.status.numberAvailable ?? "N/A"),
|
||||
},
|
||||
{
|
||||
title: "Update Strategy",
|
||||
value: objectData.spec.updateStrategy || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Created",
|
||||
value: objectData.metadata.creationTimestamp || "N/A",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const tabs: Array<Tab> = [
|
||||
{
|
||||
name: "Overview",
|
||||
children: (
|
||||
<KubernetesOverviewTab
|
||||
summaryFields={summaryFields}
|
||||
labels={objectData?.metadata.labels || {}}
|
||||
annotations={objectData?.metadata.annotations || {}}
|
||||
isLoading={isLoadingObject}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Events",
|
||||
children: (
|
||||
<Card
|
||||
title="DaemonSet Events"
|
||||
description="Kubernetes events for this daemonset in the last 24 hours."
|
||||
>
|
||||
<KubernetesEventsTab
|
||||
clusterIdentifier={clusterIdentifier}
|
||||
resourceKind="DaemonSet"
|
||||
resourceName={daemonSetName}
|
||||
namespace={objectData?.metadata.namespace}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Metrics",
|
||||
children: (
|
||||
<Card
|
||||
title={`DaemonSet Metrics: ${daemonSetName}`}
|
||||
description="CPU and memory usage for pods in this daemonset over the last 6 hours."
|
||||
>
|
||||
<KubernetesMetricsTab
|
||||
queryConfigs={[cpuQuery, memoryQuery]}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="DaemonSet" value={daemonSetName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
<div className="mb-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="DaemonSet" value={daemonSetName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title={`DaemonSet Metrics: ${daemonSetName}`}
|
||||
description="CPU and memory usage for pods in this daemonset over the last 6 hours."
|
||||
>
|
||||
<MetricView
|
||||
data={{
|
||||
...metricViewData,
|
||||
queryConfigs: [cpuQuery, memoryQuery],
|
||||
}}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: [cpuQuery, memoryQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<Tabs tabs={tabs} onTabChange={() => {}} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData, {
|
||||
ChartSeries,
|
||||
} from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
@@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
import Tabs from "Common/UI/Components/Tabs/Tabs";
|
||||
import { Tab } from "Common/UI/Components/Tabs/Tab";
|
||||
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
|
||||
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
|
||||
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
|
||||
import { KubernetesDeploymentObject } from "../Utils/KubernetesObjectParser";
|
||||
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
|
||||
|
||||
const KubernetesClusterDeploymentDetail: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -35,16 +38,9 @@ const KubernetesClusterDeploymentDetail: FunctionComponent<
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
const [objectData, setObjectData] =
|
||||
useState<KubernetesDeploymentObject | null>(null);
|
||||
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
@@ -69,6 +65,31 @@ const KubernetesClusterDeploymentDetail: FunctionComponent<
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Fetch the K8s deployment object for overview tab
|
||||
useEffect(() => {
|
||||
if (!cluster?.clusterIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchObject: () => Promise<void> = async (): Promise<void> => {
|
||||
setIsLoadingObject(true);
|
||||
try {
|
||||
const obj: KubernetesDeploymentObject | null =
|
||||
await fetchLatestK8sObject<KubernetesDeploymentObject>({
|
||||
clusterIdentifier: cluster.clusterIdentifier || "",
|
||||
resourceType: "deployments",
|
||||
resourceName: deploymentName,
|
||||
});
|
||||
setObjectData(obj);
|
||||
} catch {
|
||||
// Graceful degradation — overview tab shows empty state
|
||||
}
|
||||
setIsLoadingObject(false);
|
||||
};
|
||||
|
||||
fetchObject().catch(() => {});
|
||||
}, [cluster?.clusterIdentifier, deploymentName]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
@@ -143,32 +164,96 @@ const KubernetesClusterDeploymentDetail: FunctionComponent<
|
||||
getSeries: getSeries,
|
||||
};
|
||||
|
||||
// Build overview summary fields from deployment object
|
||||
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
|
||||
[
|
||||
{ title: "Name", value: deploymentName },
|
||||
{ title: "Cluster", value: clusterIdentifier },
|
||||
];
|
||||
|
||||
if (objectData) {
|
||||
summaryFields.push(
|
||||
{
|
||||
title: "Namespace",
|
||||
value: objectData.metadata.namespace || "default",
|
||||
},
|
||||
{
|
||||
title: "Replicas",
|
||||
value: String(objectData.spec.replicas ?? "N/A"),
|
||||
},
|
||||
{
|
||||
title: "Ready Replicas",
|
||||
value: String(objectData.status.readyReplicas ?? "N/A"),
|
||||
},
|
||||
{
|
||||
title: "Available Replicas",
|
||||
value: String(objectData.status.availableReplicas ?? "N/A"),
|
||||
},
|
||||
{
|
||||
title: "Strategy",
|
||||
value: objectData.spec.strategy || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Created",
|
||||
value: objectData.metadata.creationTimestamp || "N/A",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const tabs: Array<Tab> = [
|
||||
{
|
||||
name: "Overview",
|
||||
children: (
|
||||
<KubernetesOverviewTab
|
||||
summaryFields={summaryFields}
|
||||
labels={objectData?.metadata.labels || {}}
|
||||
annotations={objectData?.metadata.annotations || {}}
|
||||
conditions={objectData?.status.conditions}
|
||||
isLoading={isLoadingObject}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Events",
|
||||
children: (
|
||||
<Card
|
||||
title="Deployment Events"
|
||||
description="Kubernetes events for this deployment in the last 24 hours."
|
||||
>
|
||||
<KubernetesEventsTab
|
||||
clusterIdentifier={clusterIdentifier}
|
||||
resourceKind="Deployment"
|
||||
resourceName={deploymentName}
|
||||
namespace={objectData?.metadata.namespace}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Metrics",
|
||||
children: (
|
||||
<Card
|
||||
title={`Deployment Metrics: ${deploymentName}`}
|
||||
description="CPU and memory usage for pods in this deployment over the last 6 hours."
|
||||
>
|
||||
<KubernetesMetricsTab
|
||||
queryConfigs={[cpuQuery, memoryQuery]}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Deployment" value={deploymentName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
<div className="mb-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Deployment" value={deploymentName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title={`Deployment Metrics: ${deploymentName}`}
|
||||
description="CPU and memory usage for pods in this deployment over the last 6 hours."
|
||||
>
|
||||
<MetricView
|
||||
data={{
|
||||
...metricViewData,
|
||||
queryConfigs: [cpuQuery, memoryQuery],
|
||||
}}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: [cpuQuery, memoryQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<Tabs tabs={tabs} onTabChange={() => {}} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,16 +24,11 @@ import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
|
||||
interface KubernetesEvent {
|
||||
timestamp: string;
|
||||
type: string;
|
||||
reason: string;
|
||||
objectKind: string;
|
||||
objectName: string;
|
||||
namespace: string;
|
||||
message: string;
|
||||
}
|
||||
import {
|
||||
getKvValue,
|
||||
getKvStringValue,
|
||||
} from "../Utils/KubernetesObjectParser";
|
||||
import { KubernetesEvent } from "../Utils/KubernetesObjectFetcher";
|
||||
|
||||
const KubernetesClusterEvents: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -65,13 +60,18 @@ const KubernetesClusterEvents: FunctionComponent<
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -24);
|
||||
|
||||
const listResult: ListResult<Log> = await AnalyticsModelAPI.getList<Log>({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const eventsQueryOptions: any = {
|
||||
modelType: Log,
|
||||
query: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!.toString(),
|
||||
time: new InBetween<Date>(startDate, endDate),
|
||||
attributes: {
|
||||
"logAttributes.event.domain": "k8s",
|
||||
"logAttributes.k8s.resource.name": "events",
|
||||
},
|
||||
},
|
||||
limit: 200,
|
||||
limit: 500,
|
||||
skip: 0,
|
||||
select: {
|
||||
time: true,
|
||||
@@ -83,76 +83,9 @@ const KubernetesClusterEvents: FunctionComponent<
|
||||
time: SortOrder.Descending,
|
||||
},
|
||||
requestOptions: {},
|
||||
});
|
||||
|
||||
// Helper to extract a string value from OTLP kvlistValue
|
||||
const getKvValue: (
|
||||
kvList: JSONObject | undefined,
|
||||
key: string,
|
||||
) => string = (kvList: JSONObject | undefined, key: string): string => {
|
||||
if (!kvList) {
|
||||
return "";
|
||||
}
|
||||
const values: Array<JSONObject> | undefined = (kvList as JSONObject)[
|
||||
"values"
|
||||
] as Array<JSONObject> | undefined;
|
||||
if (!values) {
|
||||
return "";
|
||||
}
|
||||
for (const entry of values) {
|
||||
if (entry["key"] === key) {
|
||||
const val: JSONObject | undefined = entry["value"] as
|
||||
| JSONObject
|
||||
| undefined;
|
||||
if (!val) {
|
||||
return "";
|
||||
}
|
||||
if (val["stringValue"]) {
|
||||
return val["stringValue"] as string;
|
||||
}
|
||||
if (val["intValue"]) {
|
||||
return String(val["intValue"]);
|
||||
}
|
||||
// Nested kvlist (e.g., regarding, metadata)
|
||||
if (val["kvlistValue"]) {
|
||||
return val["kvlistValue"] as unknown as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// Helper to get nested kvlist value
|
||||
const getNestedKvValue: (
|
||||
kvList: JSONObject | undefined,
|
||||
parentKey: string,
|
||||
childKey: string,
|
||||
) => string = (
|
||||
kvList: JSONObject | undefined,
|
||||
parentKey: string,
|
||||
childKey: string,
|
||||
): string => {
|
||||
if (!kvList) {
|
||||
return "";
|
||||
}
|
||||
const values: Array<JSONObject> | undefined = (kvList as JSONObject)[
|
||||
"values"
|
||||
] as Array<JSONObject> | undefined;
|
||||
if (!values) {
|
||||
return "";
|
||||
}
|
||||
for (const entry of values) {
|
||||
if (entry["key"] === parentKey) {
|
||||
const val: JSONObject | undefined = entry["value"] as
|
||||
| JSONObject
|
||||
| undefined;
|
||||
if (val && val["kvlistValue"]) {
|
||||
return getKvValue(val["kvlistValue"] as JSONObject, childKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
const listResult: ListResult<Log> =
|
||||
await AnalyticsModelAPI.getList<Log>(eventsQueryOptions);
|
||||
|
||||
const k8sEvents: Array<KubernetesEvent> = [];
|
||||
|
||||
@@ -167,11 +100,6 @@ const KubernetesClusterEvents: FunctionComponent<
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only process k8s event logs (from k8sobjects receiver)
|
||||
if (attrs["logAttributes.event.domain"] !== "k8s") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse the body which is OTLP kvlistValue JSON
|
||||
let bodyObj: JSONObject | null = null;
|
||||
try {
|
||||
@@ -186,7 +114,6 @@ const KubernetesClusterEvents: FunctionComponent<
|
||||
continue;
|
||||
}
|
||||
|
||||
// The body has a top-level kvlistValue with "type" (ADDED/MODIFIED) and "object" keys
|
||||
const topKvList: JSONObject | undefined = bodyObj["kvlistValue"] as
|
||||
| JSONObject
|
||||
| undefined;
|
||||
@@ -195,25 +122,55 @@ const KubernetesClusterEvents: FunctionComponent<
|
||||
}
|
||||
|
||||
// Get the "object" which is the actual k8s Event
|
||||
const objectKvListRaw: string = getKvValue(topKvList, "object");
|
||||
if (!objectKvListRaw || typeof objectKvListRaw === "string") {
|
||||
const objectVal: string | JSONObject | null = getKvValue(
|
||||
topKvList,
|
||||
"object",
|
||||
);
|
||||
if (!objectVal || typeof objectVal === "string") {
|
||||
continue;
|
||||
}
|
||||
const objectKvList: JSONObject =
|
||||
objectKvListRaw as unknown as JSONObject;
|
||||
const objectKvList: JSONObject = objectVal;
|
||||
|
||||
const eventType: string = getKvValue(objectKvList, "type") || "";
|
||||
const reason: string = getKvValue(objectKvList, "reason") || "";
|
||||
const note: string = getKvValue(objectKvList, "note") || "";
|
||||
const eventType: string =
|
||||
getKvStringValue(objectKvList, "type") || "";
|
||||
const reason: string =
|
||||
getKvStringValue(objectKvList, "reason") || "";
|
||||
const note: string =
|
||||
getKvStringValue(objectKvList, "note") || "";
|
||||
|
||||
// Get regarding object details using shared parser
|
||||
const regardingKv: string | JSONObject | null = getKvValue(
|
||||
objectKvList,
|
||||
"regarding",
|
||||
);
|
||||
const regardingObj: JSONObject | undefined =
|
||||
regardingKv && typeof regardingKv !== "string"
|
||||
? regardingKv
|
||||
: undefined;
|
||||
|
||||
const objectKind: string = regardingObj
|
||||
? getKvStringValue(regardingObj, "kind")
|
||||
: "";
|
||||
const objectName: string = regardingObj
|
||||
? getKvStringValue(regardingObj, "name")
|
||||
: "";
|
||||
|
||||
const metadataKv: string | JSONObject | null = getKvValue(
|
||||
objectKvList,
|
||||
"metadata",
|
||||
);
|
||||
const metadataObj: JSONObject | undefined =
|
||||
metadataKv && typeof metadataKv !== "string"
|
||||
? metadataKv
|
||||
: undefined;
|
||||
|
||||
// Get object details from "regarding" sub-object
|
||||
const objectKind: string =
|
||||
getNestedKvValue(objectKvList, "regarding", "kind") || "";
|
||||
const objectName: string =
|
||||
getNestedKvValue(objectKvList, "regarding", "name") || "";
|
||||
const namespace: string =
|
||||
getNestedKvValue(objectKvList, "regarding", "namespace") ||
|
||||
getNestedKvValue(objectKvList, "metadata", "namespace") ||
|
||||
(regardingObj
|
||||
? getKvStringValue(regardingObj, "namespace")
|
||||
: "") ||
|
||||
(metadataObj
|
||||
? getKvStringValue(metadataObj, "namespace")
|
||||
: "") ||
|
||||
"";
|
||||
|
||||
if (eventType || reason) {
|
||||
@@ -297,7 +254,10 @@ const KubernetesClusterEvents: FunctionComponent<
|
||||
const isWarning: boolean =
|
||||
event.type.toLowerCase() === "warning";
|
||||
return (
|
||||
<tr key={index} className={isWarning ? "bg-yellow-50" : ""}>
|
||||
<tr
|
||||
key={index}
|
||||
className={isWarning ? "bg-yellow-50" : ""}
|
||||
>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
||||
{event.timestamp}
|
||||
</td>
|
||||
|
||||
@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData, {
|
||||
ChartSeries,
|
||||
} from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
@@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
import Tabs from "Common/UI/Components/Tabs/Tabs";
|
||||
import { Tab } from "Common/UI/Components/Tabs/Tab";
|
||||
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
|
||||
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
|
||||
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
|
||||
import { KubernetesJobObject } from "../Utils/KubernetesObjectParser";
|
||||
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
|
||||
|
||||
const KubernetesClusterJobDetail: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -35,16 +38,8 @@ const KubernetesClusterJobDetail: FunctionComponent<
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
const [jobObject, setJobObject] = useState<KubernetesJobObject | null>(null);
|
||||
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
@@ -69,6 +64,31 @@ const KubernetesClusterJobDetail: FunctionComponent<
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Fetch the K8s job object for overview tab
|
||||
useEffect(() => {
|
||||
if (!cluster?.clusterIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchJobObject: () => Promise<void> = async (): Promise<void> => {
|
||||
setIsLoadingObject(true);
|
||||
try {
|
||||
const obj: KubernetesJobObject | null =
|
||||
await fetchLatestK8sObject<KubernetesJobObject>({
|
||||
clusterIdentifier: cluster.clusterIdentifier || "",
|
||||
resourceType: "jobs",
|
||||
resourceName: jobName,
|
||||
});
|
||||
setJobObject(obj);
|
||||
} catch {
|
||||
// Graceful degradation — overview tab shows empty state
|
||||
}
|
||||
setIsLoadingObject(false);
|
||||
};
|
||||
|
||||
fetchJobObject().catch(() => {});
|
||||
}, [cluster?.clusterIdentifier, jobName]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
@@ -143,32 +163,109 @@ const KubernetesClusterJobDetail: FunctionComponent<
|
||||
getSeries: getSeries,
|
||||
};
|
||||
|
||||
// Build overview summary fields from job object
|
||||
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
|
||||
[
|
||||
{ title: "Job Name", value: jobName },
|
||||
{ title: "Cluster", value: clusterIdentifier },
|
||||
];
|
||||
|
||||
if (jobObject) {
|
||||
summaryFields.push(
|
||||
{
|
||||
title: "Namespace",
|
||||
value: jobObject.metadata.namespace || "default",
|
||||
},
|
||||
{
|
||||
title: "Completions",
|
||||
value: String(jobObject.spec.completions ?? "N/A"),
|
||||
},
|
||||
{
|
||||
title: "Parallelism",
|
||||
value: String(jobObject.spec.parallelism ?? "N/A"),
|
||||
},
|
||||
{
|
||||
title: "Backoff Limit",
|
||||
value: String(jobObject.spec.backoffLimit ?? "N/A"),
|
||||
},
|
||||
{
|
||||
title: "Active",
|
||||
value: String(jobObject.status.active ?? 0),
|
||||
},
|
||||
{
|
||||
title: "Succeeded",
|
||||
value: String(jobObject.status.succeeded ?? 0),
|
||||
},
|
||||
{
|
||||
title: "Failed",
|
||||
value: String(jobObject.status.failed ?? 0),
|
||||
},
|
||||
{
|
||||
title: "Start Time",
|
||||
value: jobObject.status.startTime || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Completion Time",
|
||||
value: jobObject.status.completionTime || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Created",
|
||||
value: jobObject.metadata.creationTimestamp || "N/A",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const tabs: Array<Tab> = [
|
||||
{
|
||||
name: "Overview",
|
||||
children: (
|
||||
<KubernetesOverviewTab
|
||||
summaryFields={summaryFields}
|
||||
labels={jobObject?.metadata.labels || {}}
|
||||
annotations={jobObject?.metadata.annotations || {}}
|
||||
conditions={jobObject?.status.conditions}
|
||||
isLoading={isLoadingObject}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Events",
|
||||
children: (
|
||||
<Card title="Job Events" description="Kubernetes events for this job in the last 24 hours.">
|
||||
<KubernetesEventsTab
|
||||
clusterIdentifier={clusterIdentifier}
|
||||
resourceKind="Job"
|
||||
resourceName={jobName}
|
||||
namespace={jobObject?.metadata.namespace}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Metrics",
|
||||
children: (
|
||||
<Card
|
||||
title={`Job Metrics: ${jobName}`}
|
||||
description="CPU and memory usage for pods in this job over the last 6 hours."
|
||||
>
|
||||
<KubernetesMetricsTab
|
||||
queryConfigs={[cpuQuery, memoryQuery]}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Job" value={jobName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
<div className="mb-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Job Name" value={jobName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title={`Job Metrics: ${jobName}`}
|
||||
description="CPU and memory usage for pods in this job over the last 6 hours."
|
||||
>
|
||||
<MetricView
|
||||
data={{
|
||||
...metricViewData,
|
||||
queryConfigs: [cpuQuery, memoryQuery],
|
||||
}}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: [cpuQuery, memoryQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<Tabs tabs={tabs} onTabChange={() => {}} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData, {
|
||||
ChartSeries,
|
||||
} from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
@@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
import Tabs from "Common/UI/Components/Tabs/Tabs";
|
||||
import { Tab } from "Common/UI/Components/Tabs/Tab";
|
||||
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
|
||||
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
|
||||
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
|
||||
import { KubernetesNamespaceObject } from "../Utils/KubernetesObjectParser";
|
||||
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
|
||||
|
||||
const KubernetesClusterNamespaceDetail: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -35,16 +38,9 @@ const KubernetesClusterNamespaceDetail: FunctionComponent<
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
const [namespaceObject, setNamespaceObject] =
|
||||
useState<KubernetesNamespaceObject | null>(null);
|
||||
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
@@ -69,6 +65,32 @@ const KubernetesClusterNamespaceDetail: FunctionComponent<
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Fetch the K8s namespace object for overview tab
|
||||
useEffect(() => {
|
||||
if (!cluster?.clusterIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchNamespaceObject: () => Promise<void> =
|
||||
async (): Promise<void> => {
|
||||
setIsLoadingObject(true);
|
||||
try {
|
||||
const obj: KubernetesNamespaceObject | null =
|
||||
await fetchLatestK8sObject<KubernetesNamespaceObject>({
|
||||
clusterIdentifier: cluster.clusterIdentifier || "",
|
||||
resourceType: "namespaces",
|
||||
resourceName: namespaceName,
|
||||
});
|
||||
setNamespaceObject(obj);
|
||||
} catch {
|
||||
// Graceful degradation — overview tab shows empty state
|
||||
}
|
||||
setIsLoadingObject(false);
|
||||
};
|
||||
|
||||
fetchNamespaceObject().catch(() => {});
|
||||
}, [cluster?.clusterIdentifier, namespaceName]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
@@ -143,32 +165,75 @@ const KubernetesClusterNamespaceDetail: FunctionComponent<
|
||||
getSeries: getSeries,
|
||||
};
|
||||
|
||||
// Build overview summary fields from namespace object
|
||||
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
|
||||
[
|
||||
{ title: "Namespace Name", value: namespaceName },
|
||||
{ title: "Cluster", value: clusterIdentifier },
|
||||
];
|
||||
|
||||
if (namespaceObject) {
|
||||
summaryFields.push(
|
||||
{
|
||||
title: "Status Phase",
|
||||
value: namespaceObject.status.phase || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Created",
|
||||
value: namespaceObject.metadata.creationTimestamp || "N/A",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const tabs: Array<Tab> = [
|
||||
{
|
||||
name: "Overview",
|
||||
children: (
|
||||
<KubernetesOverviewTab
|
||||
summaryFields={summaryFields}
|
||||
labels={namespaceObject?.metadata.labels || {}}
|
||||
annotations={namespaceObject?.metadata.annotations || {}}
|
||||
isLoading={isLoadingObject}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Events",
|
||||
children: (
|
||||
<Card title="Namespace Events" description="Kubernetes events for this namespace in the last 24 hours.">
|
||||
<KubernetesEventsTab
|
||||
clusterIdentifier={clusterIdentifier}
|
||||
resourceKind="Namespace"
|
||||
resourceName={namespaceName}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Metrics",
|
||||
children: (
|
||||
<Card
|
||||
title={`Namespace Metrics: ${namespaceName}`}
|
||||
description="CPU and memory usage for pods in this namespace over the last 6 hours."
|
||||
>
|
||||
<KubernetesMetricsTab
|
||||
queryConfigs={[cpuQuery, memoryQuery]}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Namespace" value={namespaceName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
<div className="mb-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Namespace" value={namespaceName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title={`Namespace Metrics: ${namespaceName}`}
|
||||
description="CPU and memory usage for pods in this namespace over the last 6 hours."
|
||||
>
|
||||
<MetricView
|
||||
data={{
|
||||
...metricViewData,
|
||||
queryConfigs: [cpuQuery, memoryQuery],
|
||||
}}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: [cpuQuery, memoryQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<Tabs tabs={tabs} onTabChange={() => {}} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,12 +4,8 @@ import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
@@ -22,6 +18,13 @@ import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import Tabs from "Common/UI/Components/Tabs/Tabs";
|
||||
import { Tab } from "Common/UI/Components/Tabs/Tab";
|
||||
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
|
||||
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
|
||||
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
|
||||
import { KubernetesNodeObject } from "../Utils/KubernetesObjectParser";
|
||||
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
|
||||
|
||||
const KubernetesClusterNodeDetail: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -32,16 +35,10 @@ const KubernetesClusterNodeDetail: FunctionComponent<
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
const [nodeObject, setNodeObject] = useState<KubernetesNodeObject | null>(
|
||||
null,
|
||||
);
|
||||
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
@@ -66,6 +63,31 @@ const KubernetesClusterNodeDetail: FunctionComponent<
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Fetch the K8s node object for overview tab
|
||||
useEffect(() => {
|
||||
if (!cluster?.clusterIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchNodeObject: () => Promise<void> = async (): Promise<void> => {
|
||||
setIsLoadingObject(true);
|
||||
try {
|
||||
const obj: KubernetesNodeObject | null =
|
||||
await fetchLatestK8sObject<KubernetesNodeObject>({
|
||||
clusterIdentifier: cluster.clusterIdentifier || "",
|
||||
resourceType: "nodes",
|
||||
resourceName: nodeName,
|
||||
});
|
||||
setNodeObject(obj);
|
||||
} catch {
|
||||
// Graceful degradation — overview tab shows empty state
|
||||
}
|
||||
setIsLoadingObject(false);
|
||||
};
|
||||
|
||||
fetchNodeObject().catch(() => {});
|
||||
}, [cluster?.clusterIdentifier, nodeName]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
@@ -200,44 +222,144 @@ const KubernetesClusterNodeDetail: FunctionComponent<
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Node Name" value={nodeName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
</div>
|
||||
// Determine node status from conditions
|
||||
const getNodeStatus: () => { label: string; isReady: boolean } = (): {
|
||||
label: string;
|
||||
isReady: boolean;
|
||||
} => {
|
||||
if (!nodeObject) {
|
||||
return { label: "Unknown", isReady: false };
|
||||
}
|
||||
const readyCondition = nodeObject.status.conditions.find(
|
||||
(c) => {
|
||||
return c.type === "Ready";
|
||||
},
|
||||
);
|
||||
if (readyCondition && readyCondition.status === "True") {
|
||||
return { label: "Ready", isReady: true };
|
||||
}
|
||||
return { label: "NotReady", isReady: false };
|
||||
};
|
||||
|
||||
<Card
|
||||
title={`Node Metrics: ${nodeName}`}
|
||||
description="CPU, memory, filesystem, and network usage for this node over the last 6 hours."
|
||||
>
|
||||
<MetricView
|
||||
data={{
|
||||
...metricViewData,
|
||||
queryConfigs: [
|
||||
// Build overview summary fields from node object
|
||||
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
|
||||
[
|
||||
{ title: "Node Name", value: nodeName },
|
||||
{ title: "Cluster", value: clusterIdentifier },
|
||||
];
|
||||
|
||||
if (nodeObject) {
|
||||
const nodeStatus = getNodeStatus();
|
||||
|
||||
summaryFields.push(
|
||||
{
|
||||
title: "Status",
|
||||
value: (
|
||||
<span
|
||||
className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
|
||||
nodeStatus.isReady
|
||||
? "bg-green-50 text-green-700"
|
||||
: "bg-red-50 text-red-700"
|
||||
}`}
|
||||
>
|
||||
{nodeStatus.label}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "OS Image",
|
||||
value: nodeObject.status.nodeInfo.osImage || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Kernel",
|
||||
value: nodeObject.status.nodeInfo.kernelVersion || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Container Runtime",
|
||||
value: nodeObject.status.nodeInfo.containerRuntimeVersion || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Kubelet Version",
|
||||
value: nodeObject.status.nodeInfo.kubeletVersion || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Architecture",
|
||||
value: nodeObject.status.nodeInfo.architecture || "N/A",
|
||||
},
|
||||
{
|
||||
title: "CPU Allocatable",
|
||||
value: nodeObject.status.allocatable["cpu"] || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Memory Allocatable",
|
||||
value: nodeObject.status.allocatable["memory"] || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Created",
|
||||
value: nodeObject.metadata.creationTimestamp || "N/A",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const tabs: Array<Tab> = [
|
||||
{
|
||||
name: "Overview",
|
||||
children: (
|
||||
<KubernetesOverviewTab
|
||||
summaryFields={summaryFields}
|
||||
labels={nodeObject?.metadata.labels || {}}
|
||||
annotations={nodeObject?.metadata.annotations || {}}
|
||||
conditions={nodeObject?.status.conditions}
|
||||
isLoading={isLoadingObject}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Events",
|
||||
children: (
|
||||
<Card
|
||||
title="Node Events"
|
||||
description="Kubernetes events for this node in the last 24 hours."
|
||||
>
|
||||
<KubernetesEventsTab
|
||||
clusterIdentifier={clusterIdentifier}
|
||||
resourceKind="Node"
|
||||
resourceName={nodeName}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Metrics",
|
||||
children: (
|
||||
<Card
|
||||
title={`Node Metrics: ${nodeName}`}
|
||||
description="CPU, memory, filesystem, and network usage for this node over the last 6 hours."
|
||||
>
|
||||
<KubernetesMetricsTab
|
||||
queryConfigs={[
|
||||
cpuQuery,
|
||||
memoryQuery,
|
||||
filesystemQuery,
|
||||
networkRxQuery,
|
||||
networkTxQuery,
|
||||
],
|
||||
}}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: [
|
||||
cpuQuery,
|
||||
memoryQuery,
|
||||
filesystemQuery,
|
||||
networkRxQuery,
|
||||
networkTxQuery,
|
||||
],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="mb-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Node Name" value={nodeName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs tabs={tabs} onTabChange={() => {}} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData, {
|
||||
ChartSeries,
|
||||
} from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
@@ -25,6 +21,15 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
import Tabs from "Common/UI/Components/Tabs/Tabs";
|
||||
import { Tab } from "Common/UI/Components/Tabs/Tab";
|
||||
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
|
||||
import KubernetesContainersTab from "../../../Components/Kubernetes/KubernetesContainersTab";
|
||||
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
|
||||
import KubernetesLogsTab from "../../../Components/Kubernetes/KubernetesLogsTab";
|
||||
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
|
||||
import { KubernetesPodObject } from "../Utils/KubernetesObjectParser";
|
||||
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
|
||||
|
||||
const KubernetesClusterPodDetail: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -35,16 +40,8 @@ const KubernetesClusterPodDetail: FunctionComponent<
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
const [podObject, setPodObject] = useState<KubernetesPodObject | null>(null);
|
||||
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
@@ -69,6 +66,31 @@ const KubernetesClusterPodDetail: FunctionComponent<
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Fetch the K8s pod object for overview/containers tabs
|
||||
useEffect(() => {
|
||||
if (!cluster?.clusterIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchPodObject: () => Promise<void> = async (): Promise<void> => {
|
||||
setIsLoadingObject(true);
|
||||
try {
|
||||
const obj: KubernetesPodObject | null =
|
||||
await fetchLatestK8sObject<KubernetesPodObject>({
|
||||
clusterIdentifier: cluster.clusterIdentifier || "",
|
||||
resourceType: "pods",
|
||||
resourceName: podName,
|
||||
});
|
||||
setPodObject(obj);
|
||||
} catch {
|
||||
// Graceful degradation — overview tab shows empty state
|
||||
}
|
||||
setIsLoadingObject(false);
|
||||
};
|
||||
|
||||
fetchPodObject().catch(() => {});
|
||||
}, [cluster?.clusterIdentifier, podName]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
@@ -89,7 +111,8 @@ const KubernetesClusterPodDetail: FunctionComponent<
|
||||
const attributes: Record<string, unknown> =
|
||||
(data["attributes"] as Record<string, unknown>) || {};
|
||||
const containerName: string =
|
||||
(attributes["resource.k8s.container.name"] as string) || "Unknown Container";
|
||||
(attributes["resource.k8s.container.name"] as string) ||
|
||||
"Unknown Container";
|
||||
return { title: containerName };
|
||||
};
|
||||
|
||||
@@ -191,42 +214,133 @@ const KubernetesClusterPodDetail: FunctionComponent<
|
||||
},
|
||||
};
|
||||
|
||||
// Build overview summary fields from pod object
|
||||
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
|
||||
[
|
||||
{ title: "Pod Name", value: podName },
|
||||
{ title: "Cluster", value: clusterIdentifier },
|
||||
];
|
||||
|
||||
if (podObject) {
|
||||
summaryFields.push(
|
||||
{
|
||||
title: "Namespace",
|
||||
value: podObject.metadata.namespace || "default",
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
value: (
|
||||
<span
|
||||
className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
|
||||
podObject.status.phase === "Running"
|
||||
? "bg-green-50 text-green-700"
|
||||
: podObject.status.phase === "Succeeded"
|
||||
? "bg-blue-50 text-blue-700"
|
||||
: podObject.status.phase === "Failed"
|
||||
? "bg-red-50 text-red-700"
|
||||
: "bg-yellow-50 text-yellow-700"
|
||||
}`}
|
||||
>
|
||||
{podObject.status.phase || "Unknown"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{ title: "Node", value: podObject.spec.nodeName || "N/A" },
|
||||
{ title: "Pod IP", value: podObject.status.podIP || "N/A" },
|
||||
{ title: "Host IP", value: podObject.status.hostIP || "N/A" },
|
||||
{
|
||||
title: "Service Account",
|
||||
value: podObject.spec.serviceAccountName || "default",
|
||||
},
|
||||
{
|
||||
title: "Created",
|
||||
value: podObject.metadata.creationTimestamp || "N/A",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const tabs: Array<Tab> = [
|
||||
{
|
||||
name: "Overview",
|
||||
children: (
|
||||
<KubernetesOverviewTab
|
||||
summaryFields={summaryFields}
|
||||
labels={podObject?.metadata.labels || {}}
|
||||
annotations={podObject?.metadata.annotations || {}}
|
||||
conditions={podObject?.status.conditions}
|
||||
ownerReferences={podObject?.metadata.ownerReferences}
|
||||
isLoading={isLoadingObject}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Containers",
|
||||
children: podObject ? (
|
||||
<KubernetesContainersTab
|
||||
containers={podObject.spec.containers}
|
||||
initContainers={podObject.spec.initContainers}
|
||||
containerStatuses={podObject.status.containerStatuses}
|
||||
initContainerStatuses={podObject.status.initContainerStatuses}
|
||||
/>
|
||||
) : isLoadingObject ? (
|
||||
<PageLoader isVisible={true} />
|
||||
) : (
|
||||
<div className="text-gray-500 text-sm p-4">
|
||||
Container details not yet available. Ensure the kubernetes-agent Helm
|
||||
chart has resourceSpecs.enabled set to true.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Events",
|
||||
children: (
|
||||
<Card title="Pod Events" description="Kubernetes events for this pod in the last 24 hours.">
|
||||
<KubernetesEventsTab
|
||||
clusterIdentifier={clusterIdentifier}
|
||||
resourceKind="Pod"
|
||||
resourceName={podName}
|
||||
namespace={podObject?.metadata.namespace}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Logs",
|
||||
children: (
|
||||
<Card title="Application Logs" description="Container logs for this pod from the last 6 hours.">
|
||||
<KubernetesLogsTab
|
||||
clusterIdentifier={clusterIdentifier}
|
||||
podName={podName}
|
||||
namespace={podObject?.metadata.namespace}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Metrics",
|
||||
children: (
|
||||
<Card
|
||||
title={`Pod Metrics: ${podName}`}
|
||||
description="CPU, memory, and container-level resource usage for this pod over the last 6 hours."
|
||||
>
|
||||
<KubernetesMetricsTab
|
||||
queryConfigs={[podCpuQuery, podMemoryQuery, cpuQuery, memoryQuery]}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Pod Name" value={podName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
<div className="mb-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Pod Name" value={podName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title={`Pod Metrics: ${podName}`}
|
||||
description="CPU, memory, and container-level resource usage for this pod over the last 6 hours."
|
||||
>
|
||||
<MetricView
|
||||
data={{
|
||||
...metricViewData,
|
||||
queryConfigs: [
|
||||
podCpuQuery,
|
||||
podMemoryQuery,
|
||||
cpuQuery,
|
||||
memoryQuery,
|
||||
],
|
||||
}}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: [
|
||||
podCpuQuery,
|
||||
podMemoryQuery,
|
||||
cpuQuery,
|
||||
memoryQuery,
|
||||
],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<Tabs tabs={tabs} onTabChange={() => {}} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData, {
|
||||
ChartSeries,
|
||||
} from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
@@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
import Tabs from "Common/UI/Components/Tabs/Tabs";
|
||||
import { Tab } from "Common/UI/Components/Tabs/Tab";
|
||||
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
|
||||
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
|
||||
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
|
||||
import { KubernetesStatefulSetObject } from "../Utils/KubernetesObjectParser";
|
||||
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
|
||||
|
||||
const KubernetesClusterStatefulSetDetail: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -35,16 +38,9 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent<
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
const [objectData, setObjectData] =
|
||||
useState<KubernetesStatefulSetObject | null>(null);
|
||||
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
@@ -69,6 +65,31 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent<
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Fetch the K8s statefulset object for overview tab
|
||||
useEffect(() => {
|
||||
if (!cluster?.clusterIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchObject: () => Promise<void> = async (): Promise<void> => {
|
||||
setIsLoadingObject(true);
|
||||
try {
|
||||
const obj: KubernetesStatefulSetObject | null =
|
||||
await fetchLatestK8sObject<KubernetesStatefulSetObject>({
|
||||
clusterIdentifier: cluster.clusterIdentifier || "",
|
||||
resourceType: "statefulsets",
|
||||
resourceName: statefulSetName,
|
||||
});
|
||||
setObjectData(obj);
|
||||
} catch {
|
||||
// Graceful degradation — overview tab shows empty state
|
||||
}
|
||||
setIsLoadingObject(false);
|
||||
};
|
||||
|
||||
fetchObject().catch(() => {});
|
||||
}, [cluster?.clusterIdentifier, statefulSetName]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
@@ -143,32 +164,102 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent<
|
||||
getSeries: getSeries,
|
||||
};
|
||||
|
||||
// Build overview summary fields from statefulset object
|
||||
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
|
||||
[
|
||||
{ title: "Name", value: statefulSetName },
|
||||
{ title: "Cluster", value: clusterIdentifier },
|
||||
];
|
||||
|
||||
if (objectData) {
|
||||
summaryFields.push(
|
||||
{
|
||||
title: "Namespace",
|
||||
value: objectData.metadata.namespace || "default",
|
||||
},
|
||||
{
|
||||
title: "Replicas",
|
||||
value: String(objectData.spec.replicas ?? "N/A"),
|
||||
},
|
||||
{
|
||||
title: "Ready Replicas",
|
||||
value: String(objectData.status.readyReplicas ?? "N/A"),
|
||||
},
|
||||
{
|
||||
title: "Service Name",
|
||||
value: objectData.spec.serviceName || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Pod Management Policy",
|
||||
value: objectData.spec.podManagementPolicy || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Update Strategy",
|
||||
value: objectData.spec.updateStrategy || "N/A",
|
||||
},
|
||||
{
|
||||
title: "Created",
|
||||
value: objectData.metadata.creationTimestamp || "N/A",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const tabs: Array<Tab> = [
|
||||
{
|
||||
name: "Overview",
|
||||
children: (
|
||||
<KubernetesOverviewTab
|
||||
summaryFields={summaryFields}
|
||||
labels={objectData?.metadata.labels || {}}
|
||||
annotations={objectData?.metadata.annotations || {}}
|
||||
isLoading={isLoadingObject}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Events",
|
||||
children: (
|
||||
<Card
|
||||
title="StatefulSet Events"
|
||||
description="Kubernetes events for this statefulset in the last 24 hours."
|
||||
>
|
||||
<KubernetesEventsTab
|
||||
clusterIdentifier={clusterIdentifier}
|
||||
resourceKind="StatefulSet"
|
||||
resourceName={statefulSetName}
|
||||
namespace={objectData?.metadata.namespace}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Metrics",
|
||||
children: (
|
||||
<Card
|
||||
title={`StatefulSet Metrics: ${statefulSetName}`}
|
||||
description="CPU and memory usage for pods in this statefulset over the last 6 hours."
|
||||
>
|
||||
<KubernetesMetricsTab
|
||||
queryConfigs={[cpuQuery, memoryQuery]}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="StatefulSet" value={statefulSetName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
<div className="mb-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard
|
||||
title="StatefulSet"
|
||||
value={statefulSetName || "Unknown"}
|
||||
/>
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title={`StatefulSet Metrics: ${statefulSetName}`}
|
||||
description="CPU and memory usage for pods in this statefulset over the last 6 hours."
|
||||
>
|
||||
<MetricView
|
||||
data={{
|
||||
...metricViewData,
|
||||
queryConfigs: [cpuQuery, memoryQuery],
|
||||
}}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: [cpuQuery, memoryQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<Tabs tabs={tabs} onTabChange={() => {}} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,6 +32,38 @@ data:
|
||||
- name: events
|
||||
mode: watch
|
||||
group: events.k8s.io
|
||||
{{- if .Values.resourceSpecs.enabled }}
|
||||
# Pull full resource specs for dashboard detail views
|
||||
- name: pods
|
||||
mode: pull
|
||||
interval: {{ .Values.resourceSpecs.interval }}
|
||||
- name: nodes
|
||||
mode: pull
|
||||
interval: {{ .Values.resourceSpecs.interval }}
|
||||
- name: namespaces
|
||||
mode: pull
|
||||
interval: {{ .Values.resourceSpecs.interval }}
|
||||
- name: deployments
|
||||
mode: pull
|
||||
interval: {{ .Values.resourceSpecs.interval }}
|
||||
group: apps
|
||||
- name: statefulsets
|
||||
mode: pull
|
||||
interval: {{ .Values.resourceSpecs.interval }}
|
||||
group: apps
|
||||
- name: daemonsets
|
||||
mode: pull
|
||||
interval: {{ .Values.resourceSpecs.interval }}
|
||||
group: apps
|
||||
- name: jobs
|
||||
mode: pull
|
||||
interval: {{ .Values.resourceSpecs.interval }}
|
||||
group: batch
|
||||
- name: cronjobs
|
||||
mode: pull
|
||||
interval: {{ .Values.resourceSpecs.interval }}
|
||||
group: batch
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.controlPlane.enabled }}
|
||||
# Scrape control plane metrics via Prometheus endpoints
|
||||
|
||||
@@ -70,6 +70,13 @@ logs:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
|
||||
# Resource spec collection — pulls full K8s resource objects (pods, nodes,
|
||||
# deployments, etc.) for displaying labels, annotations, env vars, status,
|
||||
# and other details in the dashboard.
|
||||
resourceSpecs:
|
||||
enabled: true
|
||||
interval: 300s # How often to pull full resource specs (default: 5 minutes)
|
||||
|
||||
# Collection intervals
|
||||
collectionInterval: 30s
|
||||
|
||||
|
||||
@@ -1222,6 +1222,27 @@ const HomeFeatureSet: FeatureSet = {
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/product/scheduled-maintenance",
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/product/scheduled-maintenance",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/scheduled-maintenance`, {
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
seo,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/scheduled-maintenance",
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
res.redirect("/product/scheduled-maintenance");
|
||||
},
|
||||
);
|
||||
|
||||
app.get("/product/traces", (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/product/traces",
|
||||
|
||||
@@ -438,6 +438,43 @@ export const PageSEOConfig: Record<string, PageSEOData> = {
|
||||
},
|
||||
},
|
||||
|
||||
"/product/scheduled-maintenance": {
|
||||
title:
|
||||
"Scheduled Maintenance | Plan & Communicate Downtime | OneUptime",
|
||||
description:
|
||||
"Plan, schedule, and communicate maintenance windows to your users. Notify subscribers automatically, update status pages in real-time. Open source maintenance management.",
|
||||
canonicalPath: "/product/scheduled-maintenance",
|
||||
twitterCard: "summary_large_image",
|
||||
pageType: "product",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Products", url: "/#products" },
|
||||
{
|
||||
name: "Scheduled Maintenance",
|
||||
url: "/product/scheduled-maintenance",
|
||||
},
|
||||
],
|
||||
softwareApplication: {
|
||||
name: "OneUptime Scheduled Maintenance",
|
||||
applicationCategory: "DeveloperApplication",
|
||||
operatingSystem: "Web, Cloud",
|
||||
description:
|
||||
"Schedule maintenance windows, notify subscribers via email, SMS, and webhooks, and keep your status page updated in real-time.",
|
||||
features: [
|
||||
"Maintenance scheduling",
|
||||
"Subscriber notifications",
|
||||
"Status page integration",
|
||||
"Custom maintenance states",
|
||||
"Email and SMS alerts",
|
||||
"Webhook integrations",
|
||||
"Slack and Teams notifications",
|
||||
"Maintenance timeline",
|
||||
"Affected monitors tracking",
|
||||
"Automatic state transitions",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
"/product/traces": {
|
||||
title: "Distributed Tracing | End-to-End Request Tracing | OneUptime",
|
||||
description:
|
||||
|
||||
@@ -37,6 +37,7 @@ const PAGE_CONFIG: Record<string, SitemapPageConfig> = {
|
||||
"/product/workflows": { priority: 0.9, changefreq: "weekly" },
|
||||
"/product/dashboards": { priority: 0.9, changefreq: "weekly" },
|
||||
"/product/ai-agent": { priority: 0.9, changefreq: "weekly" },
|
||||
"/product/scheduled-maintenance": { priority: 0.9, changefreq: "weekly" },
|
||||
|
||||
// Teams (Solutions) pages
|
||||
"/solutions/devops": { priority: 0.8, changefreq: "weekly" },
|
||||
|
||||
10
Home/Views/Partials/hero-cards/scheduled-maintenance.ejs
Normal file
10
Home/Views/Partials/hero-cards/scheduled-maintenance.ejs
Normal file
@@ -0,0 +1,10 @@
|
||||
<!-- Scheduled Maintenance - Teal -->
|
||||
<div class="hero-card-wrapper hero-glow-teal-wrapper h-full">
|
||||
<a href="/product/scheduled-maintenance" class="hero-card hero-glow-teal group flex flex-col items-center justify-center text-center rounded-2xl bg-white px-4 py-5 ring-1 ring-inset ring-gray-200 transition-all hover:ring-teal-300 h-full">
|
||||
<div class="flex h-11 w-11 items-center justify-center rounded-xl bg-teal-50 ring-1 ring-teal-200">
|
||||
<%- include('../icons/scheduled-maintenance') %>
|
||||
</div>
|
||||
<div class="mt-3 text-sm font-medium text-gray-900">Maintenance</div>
|
||||
<div class="mt-1 text-xs text-gray-500">Plan & communicate downtime</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -1,5 +1,2 @@
|
||||
<!-- Kubernetes Icon - Helm wheel / K8s logo style -->
|
||||
<svg class="<%= typeof iconClass !== 'undefined' ? iconClass : 'h-5 w-5' %> text-cyan-600" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path>
|
||||
<circle cx="12" cy="12" r="3" stroke-linecap="round" stroke-linejoin="round"></circle>
|
||||
</svg>
|
||||
<!-- Kubernetes Icon - Official K8s logo -->
|
||||
<img src="/img/kubernetes.svg" alt="Kubernetes" class="<%= typeof iconClass !== 'undefined' ? iconClass : 'h-5 w-5' %>">
|
||||
|
||||
|
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 166 B |
4
Home/Views/Partials/icons/scheduled-maintenance.ejs
Normal file
4
Home/Views/Partials/icons/scheduled-maintenance.ejs
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- Scheduled Maintenance Icon - Calendar with wrench -->
|
||||
<svg class="<%= typeof iconClass !== 'undefined' ? iconClass : 'h-5 w-5' %> text-teal-600" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 842 B |
@@ -1062,6 +1062,193 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Team Notifications Section -->
|
||||
<div class="relative overflow-hidden bg-white py-24 sm:py-32">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
|
||||
<!-- Content -->
|
||||
<div class="relative">
|
||||
<p class="text-sm font-medium text-cyan-600 uppercase tracking-wide mb-3">Team Notifications</p>
|
||||
<h2 class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl">
|
||||
Kubernetes alerts where your team already works
|
||||
</h2>
|
||||
<p class="mt-4 text-lg text-gray-600 leading-relaxed">
|
||||
Get instant notifications for pod crashes, node issues, and deployment failures in Slack and Microsoft Teams. Acknowledge and investigate without leaving your chat app.
|
||||
</p>
|
||||
|
||||
<!-- Integration cards -->
|
||||
<div class="mt-10 space-y-3">
|
||||
<!-- Slack -->
|
||||
<div class="flex items-center gap-4 p-4 rounded-xl bg-gray-50 border border-gray-100">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-white shadow-sm ring-1 ring-gray-200">
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z" fill="#E01E5A"/>
|
||||
<path d="M8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312z" fill="#36C5F0"/>
|
||||
<path d="M18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312z" fill="#2EB67D"/>
|
||||
<path d="M15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z" fill="#ECB22E"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold text-gray-900">Slack</h3>
|
||||
<p class="text-sm text-gray-500">Interactive Kubernetes alert actions</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Microsoft Teams -->
|
||||
<div class="flex items-center gap-4 p-4 rounded-xl bg-gray-50 border border-gray-100">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-white shadow-sm ring-1 ring-gray-200">
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20.16 7.8h-4.736c.103.317.159.654.159 1.005v7.029a3.588 3.588 0 01-3.583 3.583h-4.8v.777c0 .998.81 1.806 1.806 1.806h8.339l2.869 1.912a.452.452 0 00.703-.376V21.2h.646c.997 0 1.806-.809 1.806-1.806v-9.788A1.806 1.806 0 0020.16 7.8z" fill="#5059C9"/>
|
||||
<circle cx="18.5" cy="4.5" r="2.5" fill="#5059C9"/>
|
||||
<path d="M13.5 6H3.806A1.806 1.806 0 002 7.806v9.388c0 .997.81 1.806 1.806 1.806h2.611v3.336a.452.452 0 00.703.376L10.5 20h3a3.5 3.5 0 003.5-3.5v-7A3.5 3.5 0 0013.5 6z" fill="#7B83EB"/>
|
||||
<circle cx="10" cy="3" r="3" fill="#7B83EB"/>
|
||||
<path d="M6 11h6M6 14h4" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold text-gray-900">Microsoft Teams</h3>
|
||||
<p class="text-sm text-gray-500">Native adaptive cards integration</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features list -->
|
||||
<div class="mt-8 grid grid-cols-2 gap-4">
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600">
|
||||
<svg class="h-4 w-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Real-time delivery
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600">
|
||||
<svg class="h-4 w-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Pod crash alerts
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600">
|
||||
<svg class="h-4 w-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Rich formatting
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600">
|
||||
<svg class="h-4 w-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Action buttons
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slack Demo -->
|
||||
<div class="mt-12 lg:mt-0 relative">
|
||||
<!-- Decorative gradient background -->
|
||||
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
|
||||
|
||||
<div class="relative rounded-2xl bg-[#1a1d21] shadow-2xl ring-1 ring-white/10 overflow-hidden">
|
||||
<!-- Slack Window Header -->
|
||||
<div class="bg-[#0f1114] px-4 py-3 flex items-center justify-between border-b border-white/5">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex gap-1.5">
|
||||
<div class="w-3 h-3 rounded-full bg-[#ff5f57]"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-[#febc2e]"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-[#28c840]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="h-4 w-4 text-white/60" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/>
|
||||
</svg>
|
||||
<span class="text-white/60 text-sm font-medium">#kubernetes-alerts</span>
|
||||
</div>
|
||||
<div class="w-16"></div>
|
||||
</div>
|
||||
|
||||
<!-- Slack Messages Area -->
|
||||
<div class="p-4 space-y-4 min-h-[320px]" id="kubernetes-slack-messages">
|
||||
<!-- Main Alert Message -->
|
||||
<div class="flex gap-3" style="animation: fadeSlideIn 0.5s ease-out forwards;">
|
||||
<div class="h-10 w-10 rounded-lg bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center flex-shrink-0 shadow-lg">
|
||||
<svg class="h-6 w-6 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="font-bold text-white text-sm">OneUptime Kubernetes</span>
|
||||
<span class="text-xs text-white/40">2:47 PM</span>
|
||||
</div>
|
||||
<!-- Alert Card -->
|
||||
<div class="mt-2 rounded-lg bg-[#0f1114] border border-white/10 overflow-hidden">
|
||||
<div class="p-3 border-l-4 border-red-500">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-red-500/20 text-red-400 ring-1 ring-red-500/30">CRITICAL</span>
|
||||
<span class="text-white/40 text-xs">Pod CrashLoopBackOff</span>
|
||||
</div>
|
||||
<p class="text-white font-semibold">payment-service-6d8f9 restarting</p>
|
||||
<div class="mt-2 flex items-center gap-4 text-xs text-white/50">
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
ns: production
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Restarts: 5
|
||||
</span>
|
||||
</div>
|
||||
<!-- Action Buttons -->
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button class="px-3 py-1.5 rounded text-xs font-semibold bg-white/10 text-white hover:bg-white/20 transition-colors border border-white/10">
|
||||
Acknowledge
|
||||
</button>
|
||||
<button class="px-3 py-1.5 rounded text-xs font-semibold bg-cyan-600 text-white hover:bg-cyan-700 transition-colors">
|
||||
View Pod Logs
|
||||
</button>
|
||||
<button class="px-3 py-1.5 rounded text-xs font-semibold bg-white/10 text-white hover:bg-white/20 transition-colors border border-white/10">
|
||||
Resolve
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Reactions -->
|
||||
<div class="mt-2 flex items-center gap-1">
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-white/5 text-xs border border-white/10 hover:bg-white/10 cursor-pointer transition-colors">
|
||||
<span>👀</span>
|
||||
<span class="text-white/60">3</span>
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-white/5 text-xs border border-white/10 hover:bg-white/10 cursor-pointer transition-colors">
|
||||
<span>🚨</span>
|
||||
<span class="text-white/60">2</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Thread Replies Container -->
|
||||
<div id="kubernetes-thread-container" class="ml-12 space-y-3 hidden">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message Input Area -->
|
||||
<div class="px-4 py-3 border-t border-white/5">
|
||||
<div class="flex items-center gap-2 px-3 py-2 rounded-lg bg-[#0f1114] border border-white/10">
|
||||
<svg class="h-5 w-5 text-white/30" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<span class="text-white/30 text-sm">Message #kubernetes-alerts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('./Partials/enterprise-ready') -%>
|
||||
<%- include('features-table') -%>
|
||||
<%- include('cta') -%>
|
||||
@@ -1070,6 +1257,19 @@
|
||||
<%- include('footer') -%>
|
||||
<%- include('./Partials/video-script') -%>
|
||||
|
||||
<style>
|
||||
@keyframes fadeSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Grid glow cursor effect -->
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
@@ -109,6 +109,17 @@
|
||||
<p class="text-xs text-gray-500">Smart routing & escalations</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Scheduled Maintenance - Teal -->
|
||||
<a href="/product/scheduled-maintenance" class="group flex items-center gap-3 rounded-lg p-2.5 transition-colors hover:bg-teal-50">
|
||||
<div class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-teal-50 ring-1 ring-teal-200 group-hover:bg-teal-100 group-hover:ring-teal-300">
|
||||
<%- include('./Partials/icons/scheduled-maintenance', {iconClass: 'h-4 w-4'}) %>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Scheduled Maintenance</p>
|
||||
<p class="text-xs text-gray-500">Plan & communicate downtime</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -775,6 +786,14 @@
|
||||
<span class="text-sm font-medium text-gray-900">On-Call</span>
|
||||
</a>
|
||||
|
||||
<!-- Scheduled Maintenance - Teal -->
|
||||
<a href="/product/scheduled-maintenance" class="flex items-center gap-3 rounded-xl p-3 hover:bg-teal-50 ring-1 ring-transparent hover:ring-teal-200">
|
||||
<div class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-teal-50 ring-1 ring-teal-200">
|
||||
<%- include('./Partials/icons/scheduled-maintenance', {iconClass: 'h-4 w-4'}) %>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900">Maintenance</span>
|
||||
</a>
|
||||
|
||||
<!-- Logs - Amber -->
|
||||
<a href="/product/logs-management" class="flex items-center gap-3 rounded-xl p-3 hover:bg-amber-50 ring-1 ring-transparent hover:ring-amber-200">
|
||||
<div class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-amber-50 ring-1 ring-amber-200">
|
||||
|
||||
704
Home/Views/scheduled-maintenance.ejs
Normal file
704
Home/Views/scheduled-maintenance.ejs
Normal file
@@ -0,0 +1,704 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" id="home">
|
||||
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
|
||||
|
||||
<head>
|
||||
<title>OneUptime | Scheduled Maintenance - Plan & Communicate Downtime</title>
|
||||
<meta name="description"
|
||||
content="Plan, schedule, and communicate maintenance windows to your users. Notify subscribers automatically, update status pages in real-time, and keep stakeholders informed. Open source.">
|
||||
<%- include('head', {
|
||||
enableGoogleTagManager: typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false
|
||||
}) -%>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%- include('nav') -%>
|
||||
|
||||
<main id="main-content">
|
||||
<!-- Hero Section -->
|
||||
<div class="relative isolate overflow-hidden bg-white" id="maintenance-hero-section">
|
||||
<!-- Subtle grid pattern background -->
|
||||
<div class="absolute inset-0 -z-10 h-full w-full bg-white bg-[linear-gradient(to_right,#f0f0f0_1px,transparent_1px),linear-gradient(to_bottom,#f0f0f0_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_110%)]"></div>
|
||||
|
||||
<!-- Grid glow effect that follows cursor - teal color -->
|
||||
<div id="maintenance-grid-glow" class="absolute inset-0 -z-9 pointer-events-none" style="opacity: 0; transition: opacity 0.3s ease-out; background: linear-gradient(to right, rgba(20, 184, 166, 0.3) 1px, transparent 1px), linear-gradient(to bottom, rgba(20, 184, 166, 0.3) 1px, transparent 1px); background-size: 4rem 4rem; -webkit-mask-image: radial-gradient(circle 250px at var(--mouse-x, 50%) var(--mouse-y, 50%), black 0%, transparent 100%); mask-image: radial-gradient(circle 250px at var(--mouse-x, 50%) var(--mouse-y, 50%), black 0%, transparent 100%);"></div>
|
||||
|
||||
<div class="py-20 sm:py-28 lg:py-32">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-3xl text-center">
|
||||
<!-- Minimal badge -->
|
||||
<p class="text-sm font-medium text-teal-600 mb-4">Keep users informed during planned downtime</p>
|
||||
|
||||
<h1 class="text-4xl font-semibold tracking-tight text-gray-900 sm:text-5xl lg:text-6xl">
|
||||
Scheduled maintenance, done right
|
||||
</h1>
|
||||
<p class="mt-6 text-lg leading-8 text-gray-600 max-w-2xl mx-auto">
|
||||
Plan maintenance windows, notify subscribers automatically, and update your status page in real-time. Keep your users informed every step of the way.
|
||||
</p>
|
||||
|
||||
<div class="mt-10 flex items-center justify-center gap-x-6">
|
||||
<a href="/accounts/register"
|
||||
class="rounded-lg bg-gray-900 px-5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-800 transition-colors">
|
||||
Get started
|
||||
</a>
|
||||
<a href="/enterprise/demo" class="text-sm font-semibold text-gray-900 hover:text-gray-600 transition-colors">
|
||||
Request demo <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Subtle feature list -->
|
||||
<div class="mt-12 flex flex-wrap items-center justify-center gap-x-8 gap-y-3 text-sm text-gray-500">
|
||||
<span>Advance scheduling</span>
|
||||
<span class="hidden sm:inline text-gray-300">|</span>
|
||||
<span>Subscriber notifications</span>
|
||||
<span class="hidden sm:inline text-gray-300">|</span>
|
||||
<span>Status page integration</span>
|
||||
<span class="hidden sm:inline text-gray-300">|</span>
|
||||
<span>Custom states</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero visual - Scheduled Maintenance dashboard mockup -->
|
||||
<div class="mt-16 sm:mt-20">
|
||||
<div class="relative mx-auto max-w-5xl">
|
||||
<div class="rounded-xl bg-gray-900/5 p-1.5 ring-1 ring-gray-900/10">
|
||||
<!-- Maintenance Dashboard Mockup -->
|
||||
<div class="rounded-lg bg-white shadow-lg overflow-hidden">
|
||||
<!-- Header bar -->
|
||||
<div class="bg-gray-50 px-6 py-3 flex items-center justify-between border-b border-gray-200">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex gap-1.5">
|
||||
<div class="w-3 h-3 rounded-full bg-red-400"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-yellow-400"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-green-400"></div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="h-4 w-4 text-teal-600" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"></path>
|
||||
</svg>
|
||||
<span class="text-sm text-gray-700 font-semibold">Scheduled Maintenance</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="flex items-center gap-1.5 px-3 py-1.5 bg-teal-600 rounded-md text-xs font-medium text-white hover:bg-teal-700 transition-colors">
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
Schedule Maintenance
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard content -->
|
||||
<div class="p-5">
|
||||
<!-- Top stats row -->
|
||||
<div class="grid grid-cols-4 gap-3 mb-5">
|
||||
<div class="bg-white rounded-lg p-3.5 border border-gray-200 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider">Scheduled</div>
|
||||
<div class="w-6 h-6 rounded-md bg-blue-50 flex items-center justify-center">
|
||||
<svg class="w-3.5 h-3.5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xl font-bold text-gray-900">3</div>
|
||||
<div class="flex items-center gap-1 mt-1">
|
||||
<span class="text-[10px] text-blue-600 font-medium">Upcoming this week</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3.5 border border-gray-200 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider">Ongoing</div>
|
||||
<div class="w-6 h-6 rounded-md bg-teal-50 flex items-center justify-center">
|
||||
<svg class="w-3.5 h-3.5 text-teal-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17l-5.384-3.08A1.59 1.59 0 015 10.68V5.83c0-1.16 1.057-2 2.163-1.78l7.603 1.52A1.59 1.59 0 0116 7.16v5.06c0 .63-.37 1.2-.95 1.46l-3.63 1.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xl font-bold text-gray-900">1</div>
|
||||
<div class="flex items-center gap-1 mt-1">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-teal-500 animate-pulse"></span>
|
||||
<span class="text-[10px] text-teal-600 font-medium">In progress</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3.5 border border-gray-200 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider">Completed</div>
|
||||
<div class="w-6 h-6 rounded-md bg-emerald-50 flex items-center justify-center">
|
||||
<svg class="w-3.5 h-3.5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xl font-bold text-gray-900">12</div>
|
||||
<div class="flex items-center gap-1 mt-1">
|
||||
<span class="text-[10px] text-emerald-600 font-medium">This month</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3.5 border border-gray-200 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider">Subscribers</div>
|
||||
<div class="w-6 h-6 rounded-md bg-violet-50 flex items-center justify-center">
|
||||
<svg class="w-3.5 h-3.5 text-violet-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xl font-bold text-gray-900">847</div>
|
||||
<div class="flex items-center gap-1 mt-1">
|
||||
<span class="text-[10px] text-violet-600 font-medium">Auto-notified</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Maintenance events list -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden">
|
||||
<div class="px-4 py-2.5 bg-gray-50 border-b border-gray-200 flex items-center justify-between">
|
||||
<span class="text-xs font-semibold text-gray-700">Upcoming & Ongoing Events</span>
|
||||
<span class="text-[10px] text-gray-400">4 events</span>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100">
|
||||
<!-- Ongoing event -->
|
||||
<div class="px-4 py-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2 h-8 rounded-full bg-teal-500"></div>
|
||||
<div>
|
||||
<div class="text-xs font-medium text-gray-900">Database Migration - Production</div>
|
||||
<div class="text-[10px] text-gray-500 mt-0.5">Started 45 min ago · Est. 2 hours remaining</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex items-center gap-1 px-2 py-1 bg-teal-50 rounded text-[10px] text-teal-700 border border-teal-200 font-medium">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-teal-500 animate-pulse"></span>
|
||||
Ongoing
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Scheduled event 1 -->
|
||||
<div class="px-4 py-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2 h-8 rounded-full bg-blue-400"></div>
|
||||
<div>
|
||||
<div class="text-xs font-medium text-gray-900">SSL Certificate Renewal</div>
|
||||
<div class="text-[10px] text-gray-500 mt-0.5">Tomorrow, 2:00 AM – 2:30 AM UTC</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-1 bg-blue-50 rounded text-[10px] text-blue-700 border border-blue-200 font-medium">Scheduled</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Scheduled event 2 -->
|
||||
<div class="px-4 py-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2 h-8 rounded-full bg-blue-400"></div>
|
||||
<div>
|
||||
<div class="text-xs font-medium text-gray-900">Infrastructure Upgrade - Load Balancers</div>
|
||||
<div class="text-[10px] text-gray-500 mt-0.5">Sat, Mar 22 · 1:00 AM – 5:00 AM UTC</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-1 bg-blue-50 rounded text-[10px] text-blue-700 border border-blue-200 font-medium">Scheduled</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Scheduled event 3 -->
|
||||
<div class="px-4 py-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-2 h-8 rounded-full bg-blue-400"></div>
|
||||
<div>
|
||||
<div class="text-xs font-medium text-gray-900">API Gateway Version Upgrade</div>
|
||||
<div class="text-[10px] text-gray-500 mt-0.5">Mon, Mar 24 · 3:00 AM – 4:00 AM UTC</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-1 bg-blue-50 rounded text-[10px] text-blue-700 border border-blue-200 font-medium">Scheduled</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('logo-roll') -%>
|
||||
|
||||
<!-- How It Works Section -->
|
||||
<div class="relative bg-gray-50 py-24 sm:py-32">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl text-center mb-16">
|
||||
<p class="text-sm font-medium text-teal-600 uppercase tracking-wide mb-3">How It Works</p>
|
||||
<h2 class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl">
|
||||
From planning to completion in four steps
|
||||
</h2>
|
||||
<p class="mt-4 text-lg text-gray-600">
|
||||
Schedule maintenance windows, keep users informed, and complete your work without surprises.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-5xl">
|
||||
<!-- Connecting line for desktop -->
|
||||
<div class="hidden lg:block relative">
|
||||
<div class="absolute top-8 left-[calc(12.5%+24px)] right-[calc(12.5%+24px)] h-px bg-teal-200"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<!-- Step 1 -->
|
||||
<div class="text-center">
|
||||
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-teal-600 text-white mb-6 shadow-sm">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
|
||||
</svg>
|
||||
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-teal-600 ring-2 ring-teal-600">1</span>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-gray-900 mb-2">Schedule Event</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Create a maintenance event with start time, duration, and affected services.</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<div class="text-center">
|
||||
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-teal-600 text-white mb-6 shadow-sm">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
|
||||
</svg>
|
||||
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-teal-600 ring-2 ring-teal-600">2</span>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-gray-900 mb-2">Notify Subscribers</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Automatically notify subscribers via email, SMS, and webhooks before maintenance begins.</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 3 -->
|
||||
<div class="text-center">
|
||||
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-teal-600 text-white mb-6 shadow-sm">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.42 15.17l-5.384-3.08A1.59 1.59 0 015 10.68V5.83c0-1.16 1.057-2 2.163-1.78l7.603 1.52A1.59 1.59 0 0116 7.16v5.06c0 .63-.37 1.2-.95 1.46l-3.63 1.5z" />
|
||||
</svg>
|
||||
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-teal-600 ring-2 ring-teal-600">3</span>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-gray-900 mb-2">Post Updates</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Share real-time progress updates on your status page as work proceeds.</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 4 -->
|
||||
<div class="text-center">
|
||||
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-teal-600 text-white mb-6 shadow-sm">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-teal-600 ring-2 ring-teal-600">4</span>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-gray-900 mb-2">Complete & Resolve</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Mark maintenance complete and automatically notify subscribers that services are restored.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Why OneUptime Section -->
|
||||
<div class="relative bg-white py-24 sm:py-32 overflow-hidden">
|
||||
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<!-- Section header -->
|
||||
<div class="mx-auto max-w-2xl text-center mb-20">
|
||||
<div class="inline-flex items-center gap-2 rounded-full bg-teal-50 px-4 py-1.5 text-sm font-medium text-teal-700 ring-1 ring-inset ring-teal-600/20 mb-6">
|
||||
<svg class="h-4 w-4 text-teal-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-teal-700">Why OneUptime for Scheduled Maintenance</span>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl lg:text-5xl">
|
||||
Maintenance communication your users will appreciate
|
||||
</h2>
|
||||
<p class="mt-6 text-lg leading-8 text-gray-600">
|
||||
Proactive communication builds trust. Keep your users informed about planned downtime so they can plan accordingly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature blocks -->
|
||||
<div class="space-y-24">
|
||||
|
||||
<!-- Feature 1: Schedule & Plan -->
|
||||
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-teal-50 ring-1 ring-teal-200">
|
||||
<svg class="h-5 w-5 text-teal-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-teal-600 uppercase tracking-wide">Planning</span>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold tracking-tight text-gray-900 sm:text-3xl">Schedule maintenance with precision</h3>
|
||||
<p class="mt-4 text-lg text-gray-600">Define start and end times, select affected monitors and status pages, and set custom maintenance states to communicate exactly what is happening.</p>
|
||||
<ul class="mt-6 space-y-3">
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Set precise start and end times
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Assign affected monitors and services
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Custom maintenance states and labels
|
||||
</li>
|
||||
</ul>
|
||||
<a href="/accounts/register" class="inline-flex items-center gap-1.5 px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors mt-8">
|
||||
Get started
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-12 lg:mt-0">
|
||||
<div class="relative">
|
||||
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
|
||||
|
||||
<!-- Schedule Event Mockup -->
|
||||
<div class="relative max-w-md mx-auto">
|
||||
<div class="bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200">
|
||||
<div class="bg-gray-50 px-4 py-2.5 flex items-center gap-3 border-b border-gray-100">
|
||||
<div class="flex gap-1.5">
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
|
||||
</div>
|
||||
<div class="flex-1 text-center">
|
||||
<span class="text-xs text-gray-400">Create Maintenance Event</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 space-y-3">
|
||||
<div>
|
||||
<label class="text-[10px] font-medium text-gray-500 uppercase tracking-wider">Event Title</label>
|
||||
<div class="mt-1 px-3 py-2 border border-gray-200 rounded-lg text-sm text-gray-900 bg-white">Database Migration - Production</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="text-[10px] font-medium text-gray-500 uppercase tracking-wider">Start Time</label>
|
||||
<div class="mt-1 px-3 py-2 border border-gray-200 rounded-lg text-sm text-gray-900 bg-white">Mar 20, 2:00 AM</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] font-medium text-gray-500 uppercase tracking-wider">End Time</label>
|
||||
<div class="mt-1 px-3 py-2 border border-gray-200 rounded-lg text-sm text-gray-900 bg-white">Mar 20, 6:00 AM</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] font-medium text-gray-500 uppercase tracking-wider">Affected Monitors</label>
|
||||
<div class="mt-1 flex flex-wrap gap-1.5">
|
||||
<span class="px-2 py-1 bg-teal-50 rounded text-[10px] text-teal-700 border border-teal-200">API Server</span>
|
||||
<span class="px-2 py-1 bg-teal-50 rounded text-[10px] text-teal-700 border border-teal-200">Database</span>
|
||||
<span class="px-2 py-1 bg-teal-50 rounded text-[10px] text-teal-700 border border-teal-200">Web App</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] font-medium text-gray-500 uppercase tracking-wider">Status Pages</label>
|
||||
<div class="mt-1 flex flex-wrap gap-1.5">
|
||||
<span class="px-2 py-1 bg-blue-50 rounded text-[10px] text-blue-700 border border-blue-200">Public Status Page</span>
|
||||
<span class="px-2 py-1 bg-blue-50 rounded text-[10px] text-blue-700 border border-blue-200">Internal Status</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-full mt-2 py-2 bg-teal-600 text-white text-sm font-medium rounded-lg hover:bg-teal-700 transition-colors">Create Event</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature 2: Automatic Notifications -->
|
||||
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
|
||||
<div class="order-2 lg:order-1 mt-12 lg:mt-0">
|
||||
<div class="relative">
|
||||
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
|
||||
|
||||
<!-- Notification Mockup -->
|
||||
<div class="relative bg-white rounded-xl shadow-lg overflow-hidden max-w-md mx-auto border border-gray-200">
|
||||
<div class="bg-gray-50 px-4 py-2.5 flex items-center gap-3 border-b border-gray-100">
|
||||
<div class="flex gap-1.5">
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
|
||||
</div>
|
||||
<div class="flex-1 text-center">
|
||||
<span class="text-xs text-gray-400">Subscriber Notifications</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 space-y-3">
|
||||
<!-- Email notification -->
|
||||
<div class="p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-6 h-6 rounded bg-blue-100 flex items-center justify-center">
|
||||
<svg class="w-3.5 h-3.5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-xs font-medium text-gray-700">Email</div>
|
||||
<span class="ml-auto text-[10px] text-emerald-600 font-medium">847 sent</span>
|
||||
</div>
|
||||
<div class="text-[10px] text-gray-500">Scheduled maintenance notification sent to all subscribers</div>
|
||||
</div>
|
||||
<!-- SMS notification -->
|
||||
<div class="p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-6 h-6 rounded bg-emerald-100 flex items-center justify-center">
|
||||
<svg class="w-3.5 h-3.5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-xs font-medium text-gray-700">SMS</div>
|
||||
<span class="ml-auto text-[10px] text-emerald-600 font-medium">123 sent</span>
|
||||
</div>
|
||||
<div class="text-[10px] text-gray-500">SMS alerts sent to phone subscribers</div>
|
||||
</div>
|
||||
<!-- Webhook notification -->
|
||||
<div class="p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-6 h-6 rounded bg-violet-100 flex items-center justify-center">
|
||||
<svg class="w-3.5 h-3.5 text-violet-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-xs font-medium text-gray-700">Webhooks</div>
|
||||
<span class="ml-auto text-[10px] text-emerald-600 font-medium">5 triggered</span>
|
||||
</div>
|
||||
<div class="text-[10px] text-gray-500">Webhook payloads sent to integrations</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-1 lg:order-2">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-teal-50 ring-1 ring-teal-200">
|
||||
<svg class="h-5 w-5 text-teal-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-teal-600 uppercase tracking-wide">Notifications</span>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold tracking-tight text-gray-900 sm:text-3xl">Automatic subscriber notifications</h3>
|
||||
<p class="mt-4 text-lg text-gray-600">Notify your users before maintenance begins via email, SMS, and webhooks. Send updates during maintenance and a final notification when services are restored.</p>
|
||||
<ul class="mt-6 space-y-3">
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Email, SMS, and webhook notifications
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Pre-maintenance advance notices
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Completion notifications when resolved
|
||||
</li>
|
||||
</ul>
|
||||
<a href="/accounts/register" class="inline-flex items-center gap-1.5 px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors mt-8">
|
||||
Get started
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature 3: Status Page Integration -->
|
||||
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-teal-50 ring-1 ring-teal-200">
|
||||
<svg class="h-5 w-5 text-teal-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-teal-600 uppercase tracking-wide">Status Page</span>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold tracking-tight text-gray-900 sm:text-3xl">Seamless status page integration</h3>
|
||||
<p class="mt-4 text-lg text-gray-600">Scheduled maintenance events automatically appear on your status page, keeping your users informed with a timeline of upcoming and ongoing maintenance.</p>
|
||||
<ul class="mt-6 space-y-3">
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Automatic status page updates
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Public maintenance timeline
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Real-time progress notes
|
||||
</li>
|
||||
</ul>
|
||||
<a href="/accounts/register" class="inline-flex items-center gap-1.5 px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors mt-8">
|
||||
Get started
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-12 lg:mt-0">
|
||||
<div class="relative">
|
||||
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
|
||||
|
||||
<!-- Status Page Mockup -->
|
||||
<div class="relative max-w-md mx-auto">
|
||||
<div class="bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200">
|
||||
<div class="bg-gray-50 px-4 py-2.5 flex items-center gap-3 border-b border-gray-100">
|
||||
<div class="flex gap-1.5">
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
|
||||
</div>
|
||||
<div class="flex-1 text-center">
|
||||
<span class="text-xs text-gray-400">Status Page - Maintenance</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<svg class="h-5 w-5 text-teal-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17l-5.384-3.08A1.59 1.59 0 015 10.68V5.83c0-1.16 1.057-2 2.163-1.78l7.603 1.52A1.59 1.59 0 0116 7.16v5.06c0 .63-.37 1.2-.95 1.46l-3.63 1.5z" />
|
||||
</svg>
|
||||
<span class="text-sm font-semibold text-gray-900">Scheduled Maintenance</span>
|
||||
</div>
|
||||
<!-- Timeline -->
|
||||
<div class="space-y-4">
|
||||
<div class="relative pl-6 border-l-2 border-teal-200">
|
||||
<div class="absolute -left-[5px] top-1 w-2 h-2 rounded-full bg-teal-500"></div>
|
||||
<div class="text-[10px] text-teal-600 font-medium">In Progress</div>
|
||||
<div class="text-xs font-medium text-gray-900 mt-1">Database Migration - Production</div>
|
||||
<div class="text-[10px] text-gray-500 mt-0.5">Migration is 75% complete. No user impact detected.</div>
|
||||
<div class="text-[10px] text-gray-400 mt-1">Updated 15 minutes ago</div>
|
||||
</div>
|
||||
<div class="relative pl-6 border-l-2 border-teal-200">
|
||||
<div class="absolute -left-[5px] top-1 w-2 h-2 rounded-full bg-teal-400"></div>
|
||||
<div class="text-[10px] text-teal-600 font-medium">Started</div>
|
||||
<div class="text-xs font-medium text-gray-900 mt-1">Maintenance window opened</div>
|
||||
<div class="text-[10px] text-gray-500 mt-0.5">Beginning database migration. Expected duration: 4 hours.</div>
|
||||
<div class="text-[10px] text-gray-400 mt-1">2:00 AM UTC</div>
|
||||
</div>
|
||||
<div class="relative pl-6 border-l-2 border-gray-200">
|
||||
<div class="absolute -left-[5px] top-1 w-2 h-2 rounded-full bg-gray-300"></div>
|
||||
<div class="text-[10px] text-gray-500 font-medium">Scheduled</div>
|
||||
<div class="text-xs font-medium text-gray-900 mt-1">Maintenance announced</div>
|
||||
<div class="text-[10px] text-gray-500 mt-0.5">Subscribers notified about upcoming maintenance.</div>
|
||||
<div class="text-[10px] text-gray-400 mt-1">Mar 18, 10:00 AM UTC</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Workspace Integration Section -->
|
||||
<div class="bg-gray-50 py-24 sm:py-32">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl text-center mb-16">
|
||||
<p class="text-sm font-medium text-teal-600 uppercase tracking-wide mb-3">Integrations</p>
|
||||
<h2 class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl">
|
||||
Works with your existing tools
|
||||
</h2>
|
||||
<p class="mt-4 text-lg text-gray-600">
|
||||
Connect scheduled maintenance events to Slack, Microsoft Teams, and more. Keep your entire team in the loop.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-4xl grid grid-cols-1 gap-8 sm:grid-cols-3">
|
||||
<!-- Slack -->
|
||||
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 text-center">
|
||||
<div class="inline-flex items-center justify-center w-12 h-12 bg-gray-50 rounded-xl mb-4 ring-1 ring-gray-200">
|
||||
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z" fill="#E01E5A"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-1">Slack</h3>
|
||||
<p class="text-xs text-gray-500">Post maintenance updates directly to Slack channels</p>
|
||||
</div>
|
||||
|
||||
<!-- Microsoft Teams -->
|
||||
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 text-center">
|
||||
<div class="inline-flex items-center justify-center w-12 h-12 bg-gray-50 rounded-xl mb-4 ring-1 ring-gray-200">
|
||||
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20.625 7.5h-3.75v-1.875A1.875 1.875 0 0015 3.75h-1.875A1.875 1.875 0 0011.25 5.625V7.5H7.5a1.875 1.875 0 00-1.875 1.875v9.375A1.875 1.875 0 007.5 20.625h13.125A1.875 1.875 0 0022.5 18.75V9.375A1.875 1.875 0 0020.625 7.5z" fill="#5059C9"/>
|
||||
<circle cx="16.875" cy="5.625" r="2.625" fill="#5059C9"/>
|
||||
<path d="M13.125 9.375H1.875A1.875 1.875 0 000 11.25v6.375a1.875 1.875 0 001.875 1.875h11.25A1.875 1.875 0 0015 17.625V11.25a1.875 1.875 0 00-1.875-1.875z" fill="#7B83EB"/>
|
||||
<circle cx="7.5" cy="6.375" r="3.375" fill="#7B83EB"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-1">Microsoft Teams</h3>
|
||||
<p class="text-xs text-gray-500">Send maintenance alerts to Teams channels</p>
|
||||
</div>
|
||||
|
||||
<!-- Webhooks -->
|
||||
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 text-center">
|
||||
<div class="inline-flex items-center justify-center w-12 h-12 bg-gray-50 rounded-xl mb-4 ring-1 ring-gray-200">
|
||||
<svg class="w-6 h-6 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-1">Webhooks</h3>
|
||||
<p class="text-xs text-gray-500">Trigger custom workflows with webhook events</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('./Partials/enterprise-ready') -%>
|
||||
<%- include('features-table') -%>
|
||||
<%- include('cta') -%>
|
||||
</main>
|
||||
|
||||
<%- include('footer') -%>
|
||||
|
||||
<!-- Grid glow effect script -->
|
||||
<script>
|
||||
(function() {
|
||||
const heroSection = document.getElementById('maintenance-hero-section');
|
||||
const gridGlow = document.getElementById('maintenance-grid-glow');
|
||||
|
||||
if (heroSection && gridGlow) {
|
||||
heroSection.addEventListener('mousemove', function(e) {
|
||||
const rect = heroSection.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
gridGlow.style.setProperty('--mouse-x', x + 'px');
|
||||
gridGlow.style.setProperty('--mouse-y', y + 'px');
|
||||
gridGlow.style.opacity = '1';
|
||||
});
|
||||
|
||||
heroSection.addEventListener('mouseleave', function() {
|
||||
gridGlow.style.opacity = '0';
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user