|
|
@@ -1,9 +1,16 @@
|
|
|
import React, { useContext, useEffect, useMemo, useState } from "react";
|
|
|
import styled from "styled-components";
|
|
|
+import _ from "lodash";
|
|
|
|
|
|
import { Context } from "shared/Context";
|
|
|
import api from "shared/api";
|
|
|
-import { ChartType, ClusterType, StorageType } from "shared/types";
|
|
|
+import {
|
|
|
+ ChartType,
|
|
|
+ ClusterType,
|
|
|
+ JobStatusType,
|
|
|
+ JobStatusWithTimeType,
|
|
|
+ StorageType,
|
|
|
+} from "shared/types";
|
|
|
import { PorterUrl } from "shared/routing";
|
|
|
|
|
|
import Chart from "./Chart";
|
|
|
@@ -12,13 +19,19 @@ import { useWebsockets } from "shared/hooks/useWebsockets";
|
|
|
|
|
|
type Props = {
|
|
|
currentCluster: ClusterType;
|
|
|
+ lastRunStatus?: JobStatusType | null;
|
|
|
namespace: string;
|
|
|
// TODO Convert to enum
|
|
|
sortType: string;
|
|
|
currentView: PorterUrl;
|
|
|
};
|
|
|
|
|
|
+interface JobStatusWithTimeAndVersion extends JobStatusWithTimeType {
|
|
|
+ resource_version: number;
|
|
|
+}
|
|
|
+
|
|
|
const ChartList: React.FunctionComponent<Props> = ({
|
|
|
+ lastRunStatus,
|
|
|
namespace,
|
|
|
sortType,
|
|
|
currentView,
|
|
|
@@ -33,11 +46,17 @@ const ChartList: React.FunctionComponent<Props> = ({
|
|
|
const [controllers, setControllers] = useState<
|
|
|
Record<string, Record<string, any>>
|
|
|
>({});
|
|
|
+ const [jobStatus, setJobStatus] = useState<
|
|
|
+ Record<string, JobStatusWithTimeAndVersion>
|
|
|
+ >({});
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
const [isError, setIsError] = useState(false);
|
|
|
|
|
|
const context = useContext(Context);
|
|
|
|
|
|
+ const getChartKey = (name: string, namespace: string) =>
|
|
|
+ `${namespace}-${name}`;
|
|
|
+
|
|
|
const updateCharts = async () => {
|
|
|
try {
|
|
|
const { currentCluster, currentProject } = context;
|
|
|
@@ -85,14 +104,14 @@ const ChartList: React.FunctionComponent<Props> = ({
|
|
|
|
|
|
const wsConfig = {
|
|
|
onopen: () => {
|
|
|
- console.log("connected to chart live updates websocket");
|
|
|
+ console.log(`connected to websocket: ${websocketID}`);
|
|
|
},
|
|
|
onmessage: (evt: MessageEvent) => {
|
|
|
let event = JSON.parse(evt.data);
|
|
|
const newChart: ChartType = event.Object;
|
|
|
const isSameChart = (chart: ChartType) =>
|
|
|
- chart.name === newChart.name &&
|
|
|
- chart.namespace === newChart.namespace;
|
|
|
+ getChartKey(chart.name, chart.namespace) ===
|
|
|
+ getChartKey(newChart.name, newChart.namespace);
|
|
|
setCharts((currentCharts) => {
|
|
|
switch (event.event_type) {
|
|
|
case "ADD":
|
|
|
@@ -116,12 +135,12 @@ const ChartList: React.FunctionComponent<Props> = ({
|
|
|
},
|
|
|
|
|
|
onclose: () => {
|
|
|
- console.log("closing chart live updates websocket");
|
|
|
+ console.log(`closing websocket: ${websocketID}`);
|
|
|
},
|
|
|
|
|
|
onerror: (err: ErrorEvent) => {
|
|
|
console.log(err);
|
|
|
- closeWebsocket("helm_releases");
|
|
|
+ closeWebsocket(websocketID);
|
|
|
},
|
|
|
};
|
|
|
|
|
|
@@ -135,7 +154,7 @@ const ChartList: React.FunctionComponent<Props> = ({
|
|
|
|
|
|
const wsConfig = {
|
|
|
onopen: () => {
|
|
|
- console.log("connected to websocket");
|
|
|
+ console.log(`connected to websocket: ${kind}`);
|
|
|
},
|
|
|
onmessage: (evt: MessageEvent) => {
|
|
|
let event = JSON.parse(evt.data);
|
|
|
@@ -148,7 +167,7 @@ const ChartList: React.FunctionComponent<Props> = ({
|
|
|
}));
|
|
|
},
|
|
|
onclose: () => {
|
|
|
- console.log("closing websocket");
|
|
|
+ console.log(`closing websocket: ${kind}`);
|
|
|
},
|
|
|
onerror: (err: ErrorEvent) => {
|
|
|
console.log(err);
|
|
|
@@ -165,6 +184,76 @@ const ChartList: React.FunctionComponent<Props> = ({
|
|
|
controllers.map((kind) => setupControllerWebsocket(kind));
|
|
|
};
|
|
|
|
|
|
+ const setupJobWebsocket = (websocketID: string) => {
|
|
|
+ const kind = "job";
|
|
|
+ let { currentCluster, currentProject } = context;
|
|
|
+ const apiPath = `/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}`;
|
|
|
+
|
|
|
+ const wsConfig = {
|
|
|
+ onopen: () => {
|
|
|
+ console.log(`connected to websocket: ${websocketID}`);
|
|
|
+ },
|
|
|
+ onmessage: (evt: MessageEvent) => {
|
|
|
+ let event = JSON.parse(evt.data);
|
|
|
+ let object = event.Object;
|
|
|
+
|
|
|
+ if (_.get(object.metadata, ["annotations", "helm.sh/hook"])) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setJobStatus((currentStatus) => {
|
|
|
+ let nextStatus: JobStatusType = null;
|
|
|
+ for (const status of Object.values(JobStatusType)) {
|
|
|
+ if (_.get(object.status, status, 0) > 0) {
|
|
|
+ nextStatus = status;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const chartName =
|
|
|
+ object.metadata.labels["app.kubernetes.io/instance"];
|
|
|
+ const chartNamespace = object.metadata.namespace;
|
|
|
+ const key = getChartKey(chartName, chartNamespace);
|
|
|
+
|
|
|
+ const existingValue: JobStatusWithTimeAndVersion = _.get(
|
|
|
+ currentStatus,
|
|
|
+ key,
|
|
|
+ null
|
|
|
+ );
|
|
|
+ const newValue: JobStatusWithTimeAndVersion = {
|
|
|
+ status: nextStatus,
|
|
|
+ start_time: object.status.startTime,
|
|
|
+ resource_version: object.metadata.resourceVersion,
|
|
|
+ };
|
|
|
+
|
|
|
+ if (
|
|
|
+ !existingValue ||
|
|
|
+ newValue.resource_version > existingValue.resource_version
|
|
|
+ ) {
|
|
|
+ return {
|
|
|
+ ...currentStatus,
|
|
|
+ [key]: newValue,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return currentStatus;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ onclose: () => {
|
|
|
+ console.log(`closing websocket: ${websocketID}`);
|
|
|
+ },
|
|
|
+ onerror: (err: ErrorEvent) => {
|
|
|
+ console.log(err);
|
|
|
+ closeWebsocket(websocketID);
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ newWebsocket(websocketID, apiPath, wsConfig);
|
|
|
+
|
|
|
+ openWebsocket(websocketID);
|
|
|
+ };
|
|
|
+
|
|
|
+ // Setup basic websockets on start
|
|
|
useEffect(() => {
|
|
|
const controllers = [
|
|
|
"deployment",
|
|
|
@@ -172,11 +261,14 @@ const ChartList: React.FunctionComponent<Props> = ({
|
|
|
"daemonset",
|
|
|
"replicaset",
|
|
|
];
|
|
|
-
|
|
|
setupControllerWebsockets(controllers);
|
|
|
|
|
|
+ const jobWebsocketID = "job";
|
|
|
+ setupJobWebsocket(jobWebsocketID);
|
|
|
+
|
|
|
return () => {
|
|
|
controllers.map((controller) => closeWebsocket(controller));
|
|
|
+ closeWebsocket(jobWebsocketID);
|
|
|
};
|
|
|
}, []);
|
|
|
|
|
|
@@ -205,14 +297,29 @@ const ChartList: React.FunctionComponent<Props> = ({
|
|
|
}, [namespace, currentView]);
|
|
|
|
|
|
const filteredCharts = useMemo(() => {
|
|
|
- const result = charts.filter((chart: ChartType) => {
|
|
|
- return (
|
|
|
- (currentView == "jobs" && chart.chart.metadata.name == "job") ||
|
|
|
- ((currentView == "applications" ||
|
|
|
- currentView == "cluster-dashboard") &&
|
|
|
- chart.chart.metadata.name != "job")
|
|
|
- );
|
|
|
- });
|
|
|
+ const result = charts
|
|
|
+ .filter((chart: ChartType) => {
|
|
|
+ return (
|
|
|
+ (currentView == "jobs" && chart.chart.metadata.name == "job") ||
|
|
|
+ ((currentView == "applications" ||
|
|
|
+ currentView == "cluster-dashboard") &&
|
|
|
+ chart.chart.metadata.name != "job")
|
|
|
+ );
|
|
|
+ })
|
|
|
+ .filter((chart: ChartType) => {
|
|
|
+ if (currentView !== "jobs") {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (lastRunStatus === null) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ const status: JobStatusWithTimeAndVersion = _.get(
|
|
|
+ jobStatus,
|
|
|
+ getChartKey(chart.name, chart.namespace),
|
|
|
+ null
|
|
|
+ );
|
|
|
+ return !status || status.status === lastRunStatus;
|
|
|
+ });
|
|
|
|
|
|
if (sortType == "Newest") {
|
|
|
result.sort((a: any, b: any) =>
|
|
|
@@ -231,7 +338,7 @@ const ChartList: React.FunctionComponent<Props> = ({
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
- }, [charts, sortType]);
|
|
|
+ }, [charts, sortType, jobStatus, lastRunStatus]);
|
|
|
|
|
|
const renderChartList = () => {
|
|
|
if (isLoading || (!namespace && namespace !== "")) {
|
|
|
@@ -250,8 +357,8 @@ const ChartList: React.FunctionComponent<Props> = ({
|
|
|
return (
|
|
|
<Placeholder>
|
|
|
<i className="material-icons">category</i> No
|
|
|
- {currentView === "jobs" ? ` jobs` : ` charts`} found in this
|
|
|
- namespace.
|
|
|
+ {currentView === "jobs" ? ` jobs` : ` charts`} found with the given
|
|
|
+ filters.
|
|
|
</Placeholder>
|
|
|
);
|
|
|
}
|
|
|
@@ -259,10 +366,14 @@ const ChartList: React.FunctionComponent<Props> = ({
|
|
|
return filteredCharts.map((chart: ChartType, i: number) => {
|
|
|
return (
|
|
|
<Chart
|
|
|
- key={`${chart.namespace}-${chart.name}`}
|
|
|
+ key={getChartKey(chart.name, chart.namespace)}
|
|
|
chart={chart}
|
|
|
controllers={controllers || {}}
|
|
|
- isJob={currentView === "jobs"}
|
|
|
+ jobStatus={_.get(
|
|
|
+ jobStatus,
|
|
|
+ getChartKey(chart.name, chart.namespace),
|
|
|
+ null
|
|
|
+ )}
|
|
|
/>
|
|
|
);
|
|
|
});
|