Răsfoiți Sursa

Merge branch 'belanger/agent-v3-integration' into dev

Alexander Belanger 3 ani în urmă
părinte
comite
36068c23ec

+ 64 - 0
api/server/handlers/cluster/get_logs_revision_values.go

@@ -0,0 +1,64 @@
+package cluster
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	porter_agent "github.com/porter-dev/porter/internal/kubernetes/porter_agent/v2"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type GetLogRevisionValuesHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewGetLogRevisionValuesHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetLogRevisionValuesHandler {
+	return &GetLogRevisionValuesHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *GetLogRevisionValuesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	request := &types.GetRevisionValuesRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	agent, err := c.GetAgent(r, cluster, "")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// get agent service
+	agentSvc, err := porter_agent.GetAgentService(agent.Clientset)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	revisions, err := porter_agent.GetRevisionValues(agent.Clientset, agentSvc, request)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, revisions)
+}

+ 1 - 0
api/server/handlers/namespace/stream_pod_logs_loki.go

@@ -64,6 +64,7 @@ func (c *StreamPodLogsLokiHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	err = agent.StreamPorterAgentLokiLog([]string{
 		fmt.Sprintf("pod=%s", request.PodSelector),
 		fmt.Sprintf("namespace=%s", request.Namespace),
+		fmt.Sprintf("helm_sh_revision=%s", request.Revision),
 	}, string(startTime), request.SearchParam, 0, safeRW)
 
 	if err != nil {

+ 29 - 0
api/server/router/cluster.go

@@ -1179,6 +1179,35 @@ func getClusterRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/logs/revision_values -> cluster.NewGetLogPodValuesHandler
+	getLogRevisionValuesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/logs/revision_values", relPath),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getLogRevisionValuesHandler := cluster.NewGetLogRevisionValuesHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getLogRevisionValuesEndpoint,
+		Handler:  getLogRevisionValuesHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/events -> cluster.NewGetEventsHandler
 	getEventsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 9 - 0
api/types/incident.go

@@ -41,6 +41,7 @@ type IncidentMeta struct {
 	InvolvedObjectKind      InvolvedObjectKind `json:"involved_object_kind" form:"required"`
 	InvolvedObjectName      string             `json:"involved_object_name" form:"required"`
 	InvolvedObjectNamespace string             `json:"involved_object_namespace" form:"required"`
+	ShouldViewLogs          bool               `json:"should_view_logs"`
 }
 
 type PaginationRequest struct {
@@ -100,6 +101,7 @@ type GetLogRequest struct {
 	StartRange  *time.Time `schema:"start_range"`
 	EndRange    *time.Time `schema:"end_range"`
 	SearchParam string     `schema:"search_param"`
+	Revision    string     `schema:"revision"`
 	PodSelector string     `schema:"pod_selector" form:"required"`
 	Namespace   string     `schema:"namespace" form:"required"`
 	Direction   string     `schema:"direction"`
@@ -109,6 +111,13 @@ type GetPodValuesRequest struct {
 	StartRange  *time.Time `schema:"start_range"`
 	EndRange    *time.Time `schema:"end_range"`
 	MatchPrefix string     `schema:"match_prefix"`
+	Revision    string     `schema:"revision"`
+}
+
+type GetRevisionValuesRequest struct {
+	StartRange  *time.Time `schema:"start_range"`
+	EndRange    *time.Time `schema:"end_range"`
+	MatchPrefix string     `schema:"match_prefix"`
 }
 
 type LogLine struct {

+ 24 - 12
dashboard/package-lock.json

@@ -15781,7 +15781,8 @@
     "@icons/material": {
       "version": "0.2.4",
       "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
-      "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw=="
+      "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==",
+      "requires": {}
     },
     "@ironplans/api": {
       "version": "0.4.1",
@@ -15910,7 +15911,8 @@
     "@material-ui/types": {
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz",
-      "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A=="
+      "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==",
+      "requires": {}
     },
     "@material-ui/utils": {
       "version": "4.11.3",
@@ -16214,7 +16216,8 @@
       "version": "7.2.1",
       "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-7.2.1.tgz",
       "integrity": "sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "@types/body-parser": {
       "version": "1.19.2",
@@ -17300,13 +17303,15 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
       "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "ajv-keywords": {
       "version": "3.5.2",
       "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
       "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "anser": {
       "version": "2.1.0",
@@ -18241,7 +18246,8 @@
     "cohere-sentry": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/cohere-sentry/-/cohere-sentry-1.0.1.tgz",
-      "integrity": "sha512-OHdKcc8LED8X/JQKlMD0Zapb4rcOkPu0m11+okHouMDep1/MvyOG4JXcK4Mo3sabJT65yozc9Uo+nJfSWzaFcg=="
+      "integrity": "sha512-OHdKcc8LED8X/JQKlMD0Zapb4rcOkPu0m11+okHouMDep1/MvyOG4JXcK4Mo3sabJT65yozc9Uo+nJfSWzaFcg==",
+      "requires": {}
     },
     "collection-visit": {
       "version": "1.0.0",
@@ -20463,7 +20469,8 @@
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
       "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "ieee754": {
       "version": "1.2.1",
@@ -21246,7 +21253,8 @@
     "markdown-to-jsx": {
       "version": "7.1.3",
       "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.1.3.tgz",
-      "integrity": "sha512-jtQ6VyT7rMT5tPV0g2EJakEnXLiPksnvlYtwQsVVZ611JsWGN8bQ1tVSDX4s6JllfEH6wmsYxNjTUAMrPmNA8w=="
+      "integrity": "sha512-jtQ6VyT7rMT5tPV0g2EJakEnXLiPksnvlYtwQsVVZ611JsWGN8bQ1tVSDX4s6JllfEH6wmsYxNjTUAMrPmNA8w==",
+      "requires": {}
     },
     "material-colors": {
       "version": "1.2.6",
@@ -22295,7 +22303,8 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
       "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "postcss-modules-local-by-default": {
       "version": "4.0.0",
@@ -22687,7 +22696,8 @@
     "react-onclickoutside": {
       "version": "6.12.2",
       "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz",
-      "integrity": "sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA=="
+      "integrity": "sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==",
+      "requires": {}
     },
     "react-popper": {
       "version": "2.3.0",
@@ -22745,7 +22755,8 @@
     "react-table": {
       "version": "7.7.0",
       "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.7.0.tgz",
-      "integrity": "sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA=="
+      "integrity": "sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA==",
+      "requires": {}
     },
     "react-transition-group": {
       "version": "4.4.2",
@@ -25904,7 +25915,8 @@
       "version": "7.5.5",
       "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
       "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "xtend": {
       "version": "4.0.2",

+ 9 - 1
dashboard/src/components/porter-form/PorterFormWrapper.tsx

@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
 
 import PorterForm from "./PorterForm";
 import { InjectedProps, PorterFormData } from "./types";
@@ -24,6 +24,7 @@ type PropsType = {
   redirectTabAfterSave?: string;
   includeMetadata?: boolean;
   injectedProps?: InjectedProps;
+  overrideCurrentTab?: string;
 };
 
 const PorterFormWrapper: React.FC<PropsType> = ({
@@ -46,6 +47,7 @@ const PorterFormWrapper: React.FC<PropsType> = ({
   redirectTabAfterSave,
   includeMetadata,
   injectedProps,
+  overrideCurrentTab,
 }) => {
   const hashCode = (s: string) => {
     return s?.split("").reduce(function (a, b) {
@@ -75,6 +77,12 @@ const PorterFormWrapper: React.FC<PropsType> = ({
   // Lifted into PorterFormWrapper to allow tab to be remembered on re-render (e.g., on revision select)
   const [currentTab, setCurrentTab] = useState(getInitialTab());
 
+  useEffect(() => {
+    if (overrideCurrentTab) {
+      setCurrentTab(overrideCurrentTab);
+    }
+  }, [overrideCurrentTab]);
+
   return (
     <React.Fragment key={hashCode(JSON.stringify(formData))}>
       <PorterFormContextProvider

+ 2 - 80
dashboard/src/main/home/cluster-dashboard/chart/JobRunTable.tsx

@@ -7,6 +7,7 @@ import api from "shared/api";
 import { Context } from "shared/Context";
 import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets";
 import { useRouting } from "shared/routing";
+import { relativeDate, timeFrom } from "shared/string_utils";
 import styled from "styled-components";
 
 type Props = {
@@ -15,26 +16,6 @@ type Props = {
   sortType: "Newest" | "Oldest" | "Alphabetical";
 };
 
-export const dateFormatter = (date: string | number) => {
-  if (!date) {
-    return "N/A";
-  }
-
-  // @ts-ignore
-  const rtf = new Intl.RelativeTimeFormat("en", {
-    localeMatcher: "best fit", // other values: "lookup"
-    numeric: "auto", // other values: "auto"
-    style: "long", // other values: "short" or "narrow"
-  });
-
-  const time = timeFrom(date);
-  if (!time) {
-    return "N/A";
-  }
-
-  return rtf.format(-time.time, time.unitOfTime);
-};
-
 const runnedFor = (start: string | number, end?: string | number) => {
   const duration = timeFrom(start, end);
 
@@ -46,65 +27,6 @@ const runnedFor = (start: string | number, end?: string | number) => {
   return `${duration.time} ${unit}`;
 };
 
-function timeFrom(time: string | number, secondTime?: string | number) {
-  // Get timestamps
-  let unixTime = new Date(time).getTime();
-  if (!unixTime) return;
-
-  let now = new Date().getTime();
-
-  if (secondTime) {
-    now = new Date(secondTime).getTime();
-  }
-
-  // Calculate difference
-  let difference = unixTime / 1000 - now / 1000;
-
-  // Setup return object
-  let tfn: any = {};
-
-  // Check if time is in the past, present, or future
-  tfn.when = "now";
-  if (difference > 0) {
-    tfn.when = "future";
-  } else if (difference < -1) {
-    tfn.when = "past";
-  }
-
-  // Convert difference to absolute
-  difference = Math.abs(difference);
-
-  // Calculate time unit
-  if (difference / (60 * 60 * 24 * 365) > 1) {
-    // Years
-    tfn.unitOfTime = "years";
-    tfn.time = Math.floor(difference / (60 * 60 * 24 * 365));
-  } else if (difference / (60 * 60 * 24 * 45) > 1) {
-    // Months
-    tfn.unitOfTime = "months";
-    tfn.time = Math.floor(difference / (60 * 60 * 24 * 45));
-  } else if (difference / (60 * 60 * 24) > 1) {
-    // Days
-    tfn.unitOfTime = "days";
-    tfn.time = Math.floor(difference / (60 * 60 * 24));
-  } else if (difference / (60 * 60) > 1) {
-    // Hours
-    tfn.unitOfTime = "hours";
-    tfn.time = Math.floor(difference / (60 * 60));
-  } else if (difference / 60 > 1) {
-    // Minutes
-    tfn.unitOfTime = "minutes";
-    tfn.time = Math.floor(difference / 60);
-  } else {
-    // Seconds
-    tfn.unitOfTime = "seconds";
-    tfn.time = Math.floor(difference);
-  }
-
-  // Return time from now data
-  return tfn;
-}
-
 const JobRunTable: React.FC<Props> = ({
   lastRunStatus,
   namespace,
@@ -199,7 +121,7 @@ const JobRunTable: React.FC<Props> = ({
       },
       {
         Header: "Run at",
-        accessor: (originalRow) => dateFormatter(originalRow.status.startTime),
+        accessor: (originalRow) => relativeDate(originalRow.status.startTime),
       },
       {
         Header: "Run for",

+ 13 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -14,7 +14,7 @@ import RevisionSection from "./RevisionSection";
 import ValuesYaml from "./ValuesYaml";
 import GraphSection from "./GraphSection";
 import MetricsSection from "./metrics/MetricsSection";
-import LogsSection from "./logs-section/LogsSection";
+import LogsSection, { InitLogData } from "./logs-section/LogsSection";
 import ListSection from "./ListSection";
 import StatusSection from "./status/StatusSection";
 import SettingsSection from "./SettingsSection";
@@ -76,6 +76,8 @@ const ExpandedChart: React.FC<Props> = (props) => {
   const [isAuthorized] = useAuth();
   const [fullScreenLogs, setFullScreenLogs] = useState<boolean>(false);
   const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
+  const [logData, setLogData] = useState<InitLogData>();
+  const [overrideCurrentTab, setOverrideCurrentTab] = useState("");
 
   const {
     isStack,
@@ -97,6 +99,11 @@ const ExpandedChart: React.FC<Props> = (props) => {
     setCurrentOverlay,
   } = useContext(Context);
 
+  const renderLogsAtTimestamp = (initLogData: InitLogData) => {
+    setLogData(initLogData);
+    setOverrideCurrentTab("logs");
+  };
+
   // Retrieve full chart data (includes form and values)
   const getChartData = async (chart: ChartType) => {
     setIsLoadingChartData(true);
@@ -420,6 +427,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
             currentChart={chart}
             isFullscreen={isFullscreen}
             setIsFullscreen={setIsFullscreen}
+            initData={logData}
           />
         );
       case "metrics":
@@ -428,7 +436,9 @@ const ExpandedChart: React.FC<Props> = (props) => {
         if (DisabledNamespacesForIncidents.includes(currentChart.namespace)) {
           return null;
         }
-        return <EventsTab currentChart={chart} />;
+        return (
+          <EventsTab currentChart={chart} setLogData={renderLogsAtTimestamp} />
+        );
       case "status":
         if (isLoadingChartData) {
           return (
@@ -897,6 +907,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
                                 chart: currentChart,
                               },
                             }}
+                            overrideCurrentTab={overrideCurrentTab}
                           />
                         </BodyWrapper>
                       )}

+ 44 - 14
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventList.tsx

@@ -8,20 +8,22 @@ import danger from "assets/danger.svg";
 import document from "assets/document.svg";
 import info from "assets/info-outlined.svg";
 import status from "assets/info-circle.svg";
-import { readableDate } from "shared/string_utils";
+import { readableDate, relativeDate } from "shared/string_utils";
 import TitleSection from "components/TitleSection";
 import api from "shared/api";
 import Modal from "main/home/modals/Modal";
 import time from "assets/time.svg";
 import { Context } from "shared/Context";
+import { InitLogData } from "../logs-section/LogsSection";
 
 const iconDict: any = {};
 
 type Props = {
   filters: any;
+  setLogData?: (logData: InitLogData) => void;
 };
 
-const EventList: React.FC<Props> = ({ filters }) => {
+const EventList: React.FC<Props> = ({ filters, setLogData }) => {
   const { currentProject, currentCluster } = useContext(Context);
   const [events, setEvents] = useState([]);
   const [expandedEvent, setExpandedEvent] = useState(null);
@@ -61,6 +63,30 @@ const EventList: React.FC<Props> = ({ filters }) => {
       });
   }, [expandedEvent]);
 
+  const redirectToLogs = (incident: any) => {
+    api
+      .getIncidentEvents(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          incident_id: incident.id,
+        }
+      )
+      .then((res) => {
+        let podName = res.data?.events[0]?.pod_name;
+        let timestamp = res.data?.events[0]?.last_seen;
+        let revision = res.data?.events[0]?.revision;
+
+        setLogData({
+          podName,
+          timestamp,
+          revision,
+        });
+      });
+  };
+
   const renderExpandedEventMessage = () => {
     if (!expandedIncidentEvents) {
       return <Loading />;
@@ -80,14 +106,14 @@ const EventList: React.FC<Props> = ({ filters }) => {
         Header: "Monitors",
         columns: [
           {
-            Header: "Name",
-            accessor: "release_name",
-            width: 180,
+            Header: "Description",
+            accessor: "short_summary",
+            width: 500,
             Cell: ({ row }: CellProps<any>) => {
               return (
                 <NameWrapper>
                   <AlertIcon src={danger} />
-                  {row.original.release_name}
+                  {row.original.short_summary}
                   {row?.original && row.original.severity === "normal" ? (
                     <></>
                   ) : (
@@ -98,16 +124,11 @@ const EventList: React.FC<Props> = ({ filters }) => {
             },
           },
           {
-            Header: "Summary",
-            accessor: "short_summary",
-            width: 270,
-          },
-          {
-            Header: "Last updated",
+            Header: "Last seen",
             accessor: "updated_at",
             width: 140,
             Cell: ({ row }: CellProps<any>) => {
-              return <Flex>{readableDate(row.original.updated_at)}</Flex>;
+              return <Flex>{relativeDate(row.original.updated_at)}</Flex>;
             },
           },
           {
@@ -132,8 +153,17 @@ const EventList: React.FC<Props> = ({ filters }) => {
             accessor: "",
             width: 30,
             Cell: ({ row }: CellProps<any>) => {
+              if (!row.original.should_view_logs) {
+                return null;
+              }
+
               return (
-                <TableButton width="102px">
+                <TableButton
+                  width="102px"
+                  onClick={() => {
+                    redirectToLogs(row.original);
+                  }}
+                >
                   <Icon src={document} />
                   View logs
                 </TableButton>

+ 4 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx

@@ -6,12 +6,14 @@ import Loading from "components/Loading";
 import InfiniteScroll from "react-infinite-scroll-component";
 import { Context } from "shared/Context";
 import Dropdown from "components/Dropdown";
+import { InitLogData } from "../logs-section/LogsSection";
 
 type Props = {
   currentChart: any;
+  setLogData?: (logData: InitLogData) => void;
 };
 
-const EventsTab: React.FC<Props> = ({ currentChart }) => {
+const EventsTab: React.FC<Props> = ({ currentChart, setLogData }) => {
   const [hasPorterAgent, setHasPorterAgent] = useState(true);
   const { currentProject, currentCluster } = useContext(Context);
   const [isLoading, setIsLoading] = useState(true);
@@ -78,6 +80,7 @@ const EventsTab: React.FC<Props> = ({ currentChart }) => {
   return (
     <EventsPageWrapper>
       <EventList
+        setLogData={setLogData}
         filters={{
           release_name: currentChart.name,
           release_namespace: currentChart.namespace,

+ 24 - 5
dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx

@@ -19,11 +19,19 @@ import DateTimePicker from "components/date-time-picker/DateTimePicker";
 import dayjs from "dayjs";
 import Loading from "components/Loading";
 import _ from "lodash";
+import { ChartType } from "shared/types";
+
+export type InitLogData = {
+  podName: string;
+  timestamp: string;
+  revision: string;
+};
 
 type Props = {
-  currentChart?: any;
+  currentChart?: ChartType;
   isFullscreen: boolean;
   setIsFullscreen: (x: boolean) => void;
+  initData?: InitLogData;
 };
 
 const escapeRegExp = (str: string) => {
@@ -88,20 +96,26 @@ const LogsSection: React.FC<Props> = ({
   currentChart,
   isFullscreen,
   setIsFullscreen,
+  initData,
 }) => {
   const scrollToBottomRef = useRef<HTMLDivElement | undefined>(undefined);
   const { currentProject, currentCluster } = useContext(Context);
-  const [podFilter, setPodFilter] = useState();
-  const [podFilterOpts, setPodFilterOpts] = useState<string[]>();
+  const [podFilter, setPodFilter] = useState(initData?.podName);
+  const [podFilterOpts, setPodFilterOpts] = useState<string[]>(
+    initData ? [initData?.podName] : null
+  );
   const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
   const [searchText, setSearchText] = useState("");
   const [enteredSearchText, setEnteredSearchText] = useState("");
-  const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
+  const [selectedDate, setSelectedDate] = useState<Date | undefined>(
+    initData ? dayjs(initData?.timestamp).toDate() : undefined
+  );
 
   const { loading, logs, refresh, moveCursor, paginationInfo } = useLogs(
     podFilter,
     currentChart.namespace,
     enteredSearchText,
+    currentChart,
     selectedDate
   );
 
@@ -110,6 +124,7 @@ const LogsSection: React.FC<Props> = ({
       .getLogPodValues(
         "<TOKEN>",
         {
+          revision: currentChart.version.toString(),
           match_prefix: currentChart.name,
         },
         {
@@ -119,7 +134,11 @@ const LogsSection: React.FC<Props> = ({
       )
       .then((res) => {
         setPodFilterOpts(_.uniq(res.data ?? []));
-        setPodFilter(res.data[0]);
+
+        // only set pod filter if the current pod is not found in the resulting data
+        if (!res.data?.contains(podFilter)) {
+          setPodFilter(res.data[0]);
+        }
       });
   }, []);
 

+ 30 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs.ts

@@ -5,6 +5,7 @@ import { useContext, useEffect, useRef, useState } from "react";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { useWebsockets, NewWebsocketOptions } from "shared/hooks/useWebsockets";
+import { ChartType } from "shared/types";
 import { isJSON } from "shared/util";
 
 const MAX_LOGS = 5000;
@@ -57,12 +58,15 @@ export const useLogs = (
   currentPod: string,
   namespace: string,
   searchParam: string,
+  currentChart: ChartType,
   // if setDate is set, results are not live
   setDate?: Date
 ) => {
   const isLive = !setDate;
   const logsBufferRef = useRef<Log[]>([]);
-  const { currentCluster, currentProject } = useContext(Context);
+  const { currentCluster, currentProject, setCurrentError } = useContext(
+    Context
+  );
   const [logs, setLogs] = useState<Log[]>([]);
   const [paginationInfo, setPaginationInfo] = useState<PaginationInfo>({
     previousCursor: null,
@@ -164,7 +168,16 @@ export const useLogs = (
   };
 
   const setupWebsocket = (websocketKey: string) => {
-    const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${namespace}/logs/loki?pod_selector=${currentPod}&namespace=${namespace}&search_param=${searchParam}`;
+    const websocketBaseURL = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${namespace}/logs/loki`;
+
+    const q = new URLSearchParams({
+      pod_selector: currentPod,
+      namespace,
+      search_param: searchParam,
+      revision: currentChart.version.toString(),
+    }).toString();
+
+    const endpoint = `${websocketBaseURL}?${q}`;
 
     const config: NewWebsocketOptions = {
       onopen: () => {
@@ -196,13 +209,18 @@ export const useLogs = (
     endDate: string,
     direction: Direction,
     limit: number = QUERY_LIMIT
-  ) => {
+  ): Promise<{
+    logs: Log[];
+    previousCursor: string | null;
+    nextCursor: string | null;
+  }> => {
     return api
       .getLogs(
         "<token>",
         {
           pod_selector: currentPod,
           namespace,
+          revision: currentChart.version.toString(),
           search_param: searchParam,
           start_range: startDate,
           end_range: endDate,
@@ -232,6 +250,15 @@ export const useLogs = (
               : res.data.backward_continue_time,
           nextCursor: res.data.forward_continue_time,
         };
+      })
+      .catch((err) => {
+        setCurrentError(err);
+
+        return {
+          logs: [],
+          previousCursor: null,
+          nextCursor: null,
+        };
       });
   };
 

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

@@ -1993,6 +1993,7 @@ const getGitlabFolderContent = baseApi<
 
 const getLogPodValues = baseApi<
   {
+    revision?: string;
     match_prefix?: string;
     start_range?: string;
     end_range?: string;
@@ -2012,6 +2013,7 @@ const getLogs = baseApi<
     limit?: number;
     start_range?: string;
     end_range?: string;
+    revision?: string;
     pod_selector: string;
     namespace: string;
     search_param?: string;

+ 82 - 0
dashboard/src/shared/string_utils.ts

@@ -8,6 +8,88 @@ export const readableDate = (s: string) => {
   return `${time} on ${date}`;
 };
 
+export const relativeDate = (date: string | number) => {
+  if (!date) {
+    return "N/A";
+  }
+
+  // @ts-ignore
+  const rtf = new Intl.RelativeTimeFormat("en", {
+    localeMatcher: "best fit", // other values: "lookup"
+    numeric: "auto", // other values: "auto"
+    style: "long", // other values: "short" or "narrow"
+  });
+
+  const time = timeFrom(date);
+  if (!time) {
+    return "N/A";
+  }
+
+  return rtf.format(-time.time, time.unitOfTime);
+};
+
+export const timeFrom = (
+  time: string | number,
+  secondTime?: string | number
+) => {
+  // Get timestamps
+  let unixTime = new Date(time).getTime();
+  if (!unixTime) return;
+
+  let now = new Date().getTime();
+
+  if (secondTime) {
+    now = new Date(secondTime).getTime();
+  }
+
+  // Calculate difference
+  let difference = unixTime / 1000 - now / 1000;
+
+  // Setup return object
+  let tfn: any = {};
+
+  // Check if time is in the past, present, or future
+  tfn.when = "now";
+  if (difference > 0) {
+    tfn.when = "future";
+  } else if (difference < -1) {
+    tfn.when = "past";
+  }
+
+  // Convert difference to absolute
+  difference = Math.abs(difference);
+
+  // Calculate time unit
+  if (difference / (60 * 60 * 24 * 365) > 1) {
+    // Years
+    tfn.unitOfTime = "years";
+    tfn.time = Math.floor(difference / (60 * 60 * 24 * 365));
+  } else if (difference / (60 * 60 * 24 * 45) > 1) {
+    // Months
+    tfn.unitOfTime = "months";
+    tfn.time = Math.floor(difference / (60 * 60 * 24 * 45));
+  } else if (difference / (60 * 60 * 24) > 1) {
+    // Days
+    tfn.unitOfTime = "days";
+    tfn.time = Math.floor(difference / (60 * 60 * 24));
+  } else if (difference / (60 * 60) > 1) {
+    // Hours
+    tfn.unitOfTime = "hours";
+    tfn.time = Math.floor(difference / (60 * 60));
+  } else if (difference / 60 > 1) {
+    // Minutes
+    tfn.unitOfTime = "minutes";
+    tfn.time = Math.floor(difference / 60);
+  } else {
+    // Seconds
+    tfn.unitOfTime = "seconds";
+    tfn.time = Math.floor(difference);
+  }
+
+  // Return time from now data
+  return tfn;
+};
+
 export const capitalize = (s: string) => {
   return s.charAt(0).toUpperCase() + s.substring(1).toLowerCase();
 };

+ 54 - 0
internal/kubernetes/porter_agent/v2/agent_server.go

@@ -172,6 +172,7 @@ func GetHistoricalLogs(
 
 	vals["pod_selector"] = req.PodSelector
 	vals["namespace"] = req.Namespace
+	vals["revision"] = req.Revision
 
 	if req.SearchParam != "" {
 		vals["search_param"] = req.SearchParam
@@ -232,6 +233,7 @@ func GetPodValues(
 	}
 
 	vals["match_prefix"] = req.MatchPrefix
+	vals["revision"] = req.Revision
 
 	resp := clientset.CoreV1().Services(service.Namespace).ProxyGet(
 		"http",
@@ -256,6 +258,58 @@ func GetPodValues(
 	return valsResp, nil
 }
 
+func GetRevisionValues(
+	clientset kubernetes.Interface,
+	service *v1.Service,
+	req *types.GetRevisionValuesRequest,
+) ([]string, error) {
+	vals := make(map[string]string)
+
+	if req.StartRange != nil {
+		startVal, err := req.StartRange.MarshalText()
+
+		if err != nil {
+			return nil, err
+		}
+
+		vals["start_range"] = string(startVal)
+	}
+
+	if req.EndRange != nil {
+		endVal, err := req.EndRange.MarshalText()
+
+		if err != nil {
+			return nil, err
+		}
+
+		vals["end_range"] = string(endVal)
+	}
+
+	vals["match_prefix"] = req.MatchPrefix
+
+	resp := clientset.CoreV1().Services(service.Namespace).ProxyGet(
+		"http",
+		service.Name,
+		fmt.Sprintf("%d", service.Spec.Ports[0].Port),
+		"/logs/revision_values",
+		vals,
+	)
+
+	rawQuery, err := resp.DoRaw(context.Background())
+	if err != nil {
+		return nil, err
+	}
+
+	valsResp := make([]string, 0)
+
+	err = json.Unmarshal(rawQuery, &valsResp)
+	if err != nil {
+		return nil, err
+	}
+
+	return valsResp, nil
+}
+
 func GetHistoricalEvents(
 	clientset kubernetes.Interface,
 	service *v1.Service,