Selaa lähdekoodia

move jobs into separate view

Alexander Belanger 5 vuotta sitten
vanhempi
sitoutus
9429d8ca67

+ 21 - 17
dashboard/src/components/values-form/ValuesWrapper.tsx

@@ -13,6 +13,7 @@ type PropsType = {
   saveValuesStatus?: string | null;
   isInModal?: boolean;
   currentTab?: string; // For resetting state when flipping b/w tabs in ExpandedChart
+  renderSaveButton?: boolean;
 };
 
 type StateType = any;
@@ -110,25 +111,28 @@ export default class ValuesWrapper extends Component<PropsType, StateType> {
   };
 
   renderButton = () => {
-    let { formTabs, currentTab } = this.props;
-    let tab = formTabs.find(
-      (t: any) => t.name === currentTab || t.value === currentTab
-    );
-    if (tab && tab.context && tab.context.type === "helm/values") {
-      return (
-        <SaveButton
-          disabled={this.isDisabled() || this.props.disabled}
-          text="Deploy"
-          onClick={() => this.props.onSubmit(this.state)}
-          status={
-            this.isDisabled()
-              ? "Missing required fields"
-              : this.props.saveValuesStatus
-          }
-          makeFlush={true}
-        />
+    if (this.props.renderSaveButton) {
+      let { formTabs, currentTab } = this.props;
+      let tab = formTabs.find(
+        (t: any) => t.name === currentTab || t.value === currentTab
       );
+      if (tab && tab.context && tab.context.type === "helm/values") {
+        return (
+          <SaveButton
+            disabled={this.isDisabled() || this.props.disabled}
+            text="Deploy"
+            onClick={() => this.props.onSubmit(this.state)}
+            status={
+              this.isDisabled()
+                ? "Missing required fields"
+                : this.props.saveValuesStatus
+            }
+            makeFlush={true}
+          />
+        );
+      }
     }
+    
   };
 
   render() {

+ 3 - 1
dashboard/src/main/home/Home.tsx

@@ -280,6 +280,7 @@ class Home extends Component<PropsType, StateType> {
         <ClusterDashboard
           currentCluster={currentCluster}
           setSidebar={(x: boolean) => this.setState({ forceSidebar: x })}
+          currentView={this.props.currentRoute}
           // setCurrentView={(x: string) => this.setState({ currentView: x })}
         />
       </DashboardWrapper>
@@ -288,8 +289,9 @@ class Home extends Component<PropsType, StateType> {
 
   renderContents = () => {
     let currentView = this.props.currentRoute;
+    
     if (this.context.currentProject && currentView !== "new-project") {
-      if (currentView === "cluster-dashboard") {
+      if (currentView === "cluster-dashboard" || currentView === "applications" || currentView === "jobs") {
         return this.renderDashboard();
       } else if (currentView === "dashboard") {
         return (

+ 20 - 13
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -4,10 +4,12 @@ import gradient from "assets/gradient.jpg";
 
 import { Context } from "shared/Context";
 import { ChartType, ClusterType } from "shared/types";
+import { PorterUrl } from "shared/routing";
 
 import ChartList from "./chart/ChartList";
 import NamespaceSelector from "./NamespaceSelector";
 import SortSelector from "./SortSelector";
+import ExpandedChart from "./expanded-chart/ExpandedChart";
 import ExpandedJobChart from "./expanded-chart/ExpandedJobChart";
 import { RouteComponentProps, withRouter } from "react-router";
 
@@ -16,6 +18,7 @@ import api from "shared/api";
 type PropsType = RouteComponentProps & {
   currentCluster: ClusterType;
   setSidebar: (x: boolean) => void;
+  currentView: PorterUrl;
 };
 
 type StateType = {
@@ -89,27 +92,30 @@ class ClusterDashboard extends Component<PropsType, StateType> {
   };
 
   renderContents = () => {
-    let { currentCluster, setSidebar } = this.props;
-
-    if (this.state.currentChart) {
+    let { currentCluster, setSidebar, currentView } = this.props;
+    
+    if (this.state.currentChart && currentView == "jobs") {
       return (
-        // <ExpandedChart
-        //   namespace={this.state.namespace}
-        //   currentCluster={this.props.currentCluster}
-        //   currentChart={this.state.currentChart}
-        //   setCurrentChart={(x: ChartType | null) =>
-        //     this.setState({ currentChart: x })
-        //   }
-        //   isMetricsInstalled={this.state.isMetricsInstalled}
-        //   setSidebar={setSidebar}
-        // />
         <ExpandedJobChart
+        namespace={this.state.namespace}
+        currentCluster={this.props.currentCluster}
+        currentChart={this.state.currentChart}
+        setCurrentChart={(x: ChartType | null) =>
+          this.setState({ currentChart: x })
+        }
+        setSidebar={setSidebar}
+      />
+      )
+    } else if (this.state.currentChart) {
+      return (
+        <ExpandedChart
           namespace={this.state.namespace}
           currentCluster={this.props.currentCluster}
           currentChart={this.state.currentChart}
           setCurrentChart={(x: ChartType | null) =>
             this.setState({ currentChart: x })
           }
+          isMetricsInstalled={this.state.isMetricsInstalled}
           setSidebar={setSidebar}
         />
       );
@@ -158,6 +164,7 @@ class ClusterDashboard extends Component<PropsType, StateType> {
         </ControlRow>
 
         <ChartList
+          currentView={currentView}
           currentCluster={currentCluster}
           namespace={this.state.namespace}
           sortType={this.state.sortType}

+ 13 - 1
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -4,6 +4,7 @@ import styled from "styled-components";
 import { Context } from "shared/Context";
 import api from "shared/api";
 import { ChartType, StorageType, ClusterType } from "shared/types";
+import { PorterUrl } from "shared/routing";
 
 import Chart from "./Chart";
 import Loading from "components/Loading";
@@ -13,6 +14,7 @@ type PropsType = {
   namespace: string;
   sortType: string;
   setCurrentChart: (c: ChartType) => void;
+  currentView: PorterUrl;
 };
 
 type StateType = {
@@ -63,6 +65,15 @@ export default class ChartList extends Component<PropsType, StateType> {
       )
       .then(res => {
         let charts = res.data || [];
+
+        // filter charts based on the current view
+        let { currentView } = this.props
+
+        charts = charts.filter((chart: ChartType) => {
+          return (currentView == "jobs" && chart.chart.metadata.name == "job") || 
+          ((currentView == "applications" || currentView == "cluster-dashboard") && chart.chart.metadata.name != "job")
+        })
+
         if (this.props.sortType == "Newest") {
           charts.sort((a: any, b: any) =>
             Date.parse(a.info.last_deployed) > Date.parse(b.info.last_deployed)
@@ -223,7 +234,8 @@ export default class ChartList extends Component<PropsType, StateType> {
     if (
       prevProps.currentCluster !== this.props.currentCluster ||
       prevProps.namespace !== this.props.namespace ||
-      prevProps.sortType !== this.props.sortType
+      prevProps.sortType !== this.props.sortType ||
+      prevProps.currentView !== this.props.currentView
     ) {
       this.updateCharts(this.getControllers);
     }

+ 1 - 9
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -292,15 +292,6 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       case "metrics":
         return <MetricsSection currentChart={chart} />;
       case "status":
-        let activeJobs = Object.values(this.state.controllers)[0]?.status
-          .active;
-        let selectors = activeJobs?.map((job: any) => {
-          return `job-name=${job.name},controller-uid=${job.uid}`;
-        });
-
-        if (chart.chart.metadata.name == "job") {
-          return <StatusSection currentChart={chart} selectors={selectors} />;
-        }
         return <StatusSection currentChart={chart} />;
       case "settings":
         return (
@@ -344,6 +335,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
               saveValuesStatus={this.state.saveValuesStatus}
               isInModal={true}
               currentTab={currentTab}
+              renderSaveButton={true}
             >
               {(metaState: any, setMetaState: any) => {
                 return tabOptions.map((tab: any, i: number) => {

+ 16 - 6
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -92,14 +92,12 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
       let exists = false
       jobs.forEach((job : any, i : number, self : any[]) => {
           if (job.metadata?.name == newJob.metadata?.name && job.metadata?.namespace == newJob.metadata?.namespace) {
-              console.log("GOT MATCH")
               self[i] = newJob
               exists = true
           }
       })
 
       if (!exists) {
-          console.log("NEED TO PUSH", newJob)
         jobs.push(newJob)
       }
 
@@ -127,8 +125,9 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
       if (event.event_type == "ADD" || event.event_type == "UPDATE") {
           // filter job belonging to chart
           let chartLabel = event.Object?.metadata?.labels["helm.sh/chart"]
+          let releaseLabel = event.Object?.metadata?.labels["meta.helm.sh/release-name"]
 
-          if (chartLabel && chartLabel == chartVersion) {
+          if (chartLabel && releaseLabel && chartLabel == chartVersion && releaseLabel == chart.name) {
             this.mergeNewJob(event.Object)
           }
       }
@@ -192,7 +191,7 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
       .catch(err => {
         console.log(err);
         this.setState({ saveValuesStatus: "error" });
-        setCurrentError(err)
+        setCurrentError(JSON.stringify(err))
       });
   };
 
@@ -208,6 +207,7 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
           id: currentProject.id,
           chart: `${chart.chart.metadata.name}-${chart.chart.metadata.version}`,
           namespace: chart.namespace,
+          release_name: chart.name,
         }
       ).then(res => {
           // sort jobs by started timestamp
@@ -240,7 +240,7 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
                 <JobList jobs={this.state.jobs} />
                 <SaveButton
                     text="Rerun Job"
-                    onClick={this.handleSaveValues}
+                    onClick={() => this.handleSaveValues()}
                     status={this.state.saveValuesStatus}
                     makeFlush={true}
                 />
@@ -259,12 +259,14 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
       default:
             if (this.state.tabOptions && currentTab && currentTab.includes("@")) {
               return (
-                <ValuesWrapper
+                <TabWrapper>
+                    <ValuesWrapper
                   formTabs={this.state.tabOptions}
                   onSubmit={this.handleSaveValues}
                   saveValuesStatus={this.state.saveValuesStatus}
                   isInModal={true}
                   currentTab={currentTab}
+                  renderSaveButton={false}
                 >
                   {(metaState: any, setMetaState: any) => {
                     return this.state.tabOptions.map((tab: any, i: number) => {
@@ -283,6 +285,14 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
                     });
                   }}
                 </ValuesWrapper>
+                <SaveButton
+                    text="Rerun Job"
+                    onClick={() => this.handleSaveValues()}
+                    status={this.state.saveValuesStatus}
+                    makeFlush={true}
+                />
+            </TabWrapper>
+                
               );
             }
     }

+ 3 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx

@@ -46,7 +46,7 @@ export default class JobResource extends Component<PropsType, StateType> {
           name: this.props.job.metadata?.name,
           namespace: this.props.job.metadata?.namespace,
         }
-      ).then(res => {
+      ).then(res => {          
           this.setState({ pods: res.data })
           callback()
       })
@@ -86,7 +86,7 @@ export default class JobResource extends Component<PropsType, StateType> {
         }
     })
 
-    return `Failed at ${this.readableDate(failedCondition.lastTransitionTime)}`
+    return failedCondition ? `Failed at ${this.readableDate(failedCondition.lastTransitionTime)}` : "Failed"
 }
 
   renderLogsSection = () => {
@@ -94,7 +94,7 @@ export default class JobResource extends Component<PropsType, StateType> {
           return <JobLogsWrapper>
               <Logs 
                 selectedPod={this.state.pods[0]}
-                podError={""}
+                podError={!this.state.pods[0] ? "Pod no longer exists." : ""}
                 rawText={true}
               />
           </JobLogsWrapper>

+ 11 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx

@@ -37,7 +37,15 @@ export default class Logs extends Component<PropsType, StateType> {
   };
 
   renderLogs = () => {
-    let { selectedPod } = this.props;
+    let { selectedPod, podError } = this.props;
+
+    if (podError && podError != "") {
+      return (
+        <Message>
+          {this.props.podError}
+        </Message>
+      );
+    }
 
     if (!selectedPod?.metadata?.name) {
       return <Message>Please select a pod to view its logs.</Message>;
@@ -54,7 +62,7 @@ export default class Logs extends Component<PropsType, StateType> {
     if (this.state.logs.length == 0) {
       return (
         <Message>
-          {this.props.podError || "No logs to display from this pod."}
+          No logs to display from this pod.
         </Message>
       );
     }
@@ -66,7 +74,7 @@ export default class Logs extends Component<PropsType, StateType> {
   setupWebsocket = () => {
     let { currentCluster, currentProject } = this.context;
     let { selectedPod } = this.props;
-    if (!selectedPod.metadata?.name) return;
+    if (!selectedPod?.metadata?.name) return;
     let protocol = process.env.NODE_ENV == "production" ? "wss" : "ws";
     this.ws = new WebSocket(
       `${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/k8s/${selectedPod?.metadata?.namespace}/pod/${selectedPod?.metadata?.name}/logs?cluster_id=${currentCluster.id}&service_account_id=${currentCluster.service_account_id}`

+ 1 - 0
dashboard/src/main/home/launch/expanded-template/LaunchTemplate.tsx

@@ -388,6 +388,7 @@ class LaunchTemplate extends Component<PropsType, StateType> {
         }
         saveValuesStatus={this.getStatus()}
         disabled={this.submitIsDisabled()}
+        renderSaveButton={true}
       >
         {(metaState: any, setMetaState: any) => {
           return this.props.form?.tabs.map((tab: any, i: number) => {

+ 31 - 0
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -93,6 +93,36 @@ class Sidebar extends Component<PropsType, StateType> {
     }
   };
 
+  renderClusterContent = () => {
+    let { currentView } = this.props;
+    let { currentCluster } = this.context;
+
+    if (currentCluster) {
+      return (
+        <>
+          <NavButton
+            selected={currentView === "applications"}
+            onClick={() => {
+              this.props.history.push("/applications");
+            }}
+          >
+            <Img src={integrations} />
+            Applications
+          </NavButton>
+          <NavButton
+            selected={currentView === "jobs"}
+            onClick={() => {
+              this.props.history.push("/jobs");
+            }}
+          >
+            <Img src={integrations} />
+            Jobs
+          </NavButton>
+        </>
+      )
+    }
+  }
+
   renderProjectContents = () => {
     let { currentView } = this.props;
     let { currentProject, setCurrentModal } = this.context;
@@ -155,6 +185,7 @@ class Sidebar extends Component<PropsType, StateType> {
             forceRefreshClusters={this.props.forceRefreshClusters}
             setRefreshClusters={this.props.setRefreshClusters}
           />
+          {this.renderClusterContent()}
         </>
       );
     }

+ 2 - 2
dashboard/src/shared/api.tsx

@@ -420,9 +420,9 @@ const getJobs = baseApi<
 {
   cluster_id: number;
 },
-{ chart: string; namespace: string; id: number }
+{ chart: string; namespace: string; release_name: string, id: number }
 >("GET", pathParams => {
-return `/api/projects/${pathParams.id}/k8s/${pathParams.namespace}/${pathParams.chart}/jobs`;
+return `/api/projects/${pathParams.id}/k8s/${pathParams.namespace}/${pathParams.chart}/${pathParams.release_name}/jobs`;
 });
 
 const getJobPods = baseApi<

+ 6 - 2
dashboard/src/shared/routing.tsx

@@ -6,7 +6,9 @@ export type PorterUrl =
   | "integrations"
   | "new-project"
   | "cluster-dashboard"
-  | "project-settings";
+  | "project-settings"
+  | "applications"
+  | "jobs";
 
 export const PorterUrls = [
   "dashboard",
@@ -14,7 +16,9 @@ export const PorterUrls = [
   "integrations",
   "new-project",
   "cluster-dashboard",
-  "project-settings"
+  "project-settings",
+  "applications",
+  "jobs",
 ];
 
 export const setSearchParam = (

+ 13 - 2
internal/kubernetes/agent.go

@@ -67,11 +67,22 @@ func (a *Agent) ListNamespaces() (*v1.NamespaceList, error) {
 }
 
 // ListJobsByLabel lists jobs in a namespace matching a label
-func (a *Agent) ListJobsByLabel(namespace, labelKey, labelVal string) ([]batchv1.Job, error) {
+type Label struct {
+	Key string
+	Val string
+}
+
+func (a *Agent) ListJobsByLabel(namespace string, labels ...Label) ([]batchv1.Job, error) {
+	selectors := make([]string, 0)
+
+	for _, label := range labels {
+		selectors = append(selectors, fmt.Sprintf("%s=%s", label.Key, label.Val))
+	}
+
 	resp, err := a.Clientset.BatchV1().Jobs(namespace).List(
 		context.TODO(),
 		metav1.ListOptions{
-			LabelSelector: fmt.Sprintf("%s=%s", labelKey, labelVal),
+			LabelSelector: strings.Join(selectors, ","),
 		},
 	)
 

+ 8 - 1
server/api/k8s_handler.go

@@ -266,6 +266,7 @@ func (app *App) HandleListJobsByChart(w http.ResponseWriter, r *http.Request) {
 	// get path parameters
 	namespace := chi.URLParam(r, "namespace")
 	chart := chi.URLParam(r, "chart")
+	releaseName := chi.URLParam(r, "release_name")
 
 	vals, err := url.ParseQuery(r.URL.RawQuery)
 
@@ -299,7 +300,13 @@ func (app *App) HandleListJobsByChart(w http.ResponseWriter, r *http.Request) {
 		agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
 	}
 
-	jobs, err := agent.ListJobsByLabel(namespace, "helm.sh/chart", chart)
+	jobs, err := agent.ListJobsByLabel(namespace, kubernetes.Label{
+		Key: "helm.sh/chart",
+		Val: chart,
+	}, kubernetes.Label{
+		Key: "meta.helm.sh/release-name",
+		Val: releaseName,
+	})
 
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)

+ 1 - 1
server/router/router.go

@@ -1142,7 +1142,7 @@ func New(a *api.App) *chi.Mux {
 
 		r.Method(
 			"GET",
-			"/projects/{project_id}/k8s/{namespace}/{chart}/jobs",
+			"/projects/{project_id}/k8s/{namespace}/{chart}/{release_name}/jobs",
 			auth.DoesUserHaveProjectAccess(
 				auth.DoesUserHaveClusterAccess(
 					requestlog.NewHandler(a.HandleListJobsByChart, l),