Feroze Mohideen 2 lat temu
rodzic
commit
70591a8647

+ 1 - 0
api/server/handlers/porter_app/create_and_update_events.go

@@ -277,6 +277,7 @@ func (p *CreateUpdatePorterAppEventHandler) maybeUpdateDeployEvent(ctx context.C
 		return types.PorterAppEvent{}
 	}
 
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "existing-status", Value: serviceStatusMap[serviceName]})
 	// only update service status if it has not been updated yet
 	if serviceStatusMap[serviceName] == "PROGRESSING" {
 		serviceStatusMap[serviceName] = newStatusStr

+ 3 - 0
dashboard/src/assets/canceled.svg

@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11 1L1 11M11 11L1 0.999998" stroke="#FFBF00" stroke-width="1" stroke-linecap="round"/>
+</svg>

+ 3 - 0
dashboard/src/assets/failure.svg

@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11 1L1 11M11 11L1 0.999998" stroke="#FF6060" stroke-width="1" stroke-linecap="round"/>
+</svg>

+ 1 - 0
dashboard/src/assets/filter-outline-icon.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512 410.73"><path fill-rule="nonzero" d="M335.62 410.73H164.96V239.89L13.31 59.96C7.33 52.52 3.19 44.79 1.29 37.65c-1.79-6.72-1.76-13.28.34-19.1 2.3-6.44 6.92-11.63 13.91-14.9C20.35 1.41 26.3.13 33.4.1L472.7.04c7.93-.29 14.95.96 20.74 3.44 7.02 2.97 12.28 7.87 15.44 14.17 3.05 6.1 3.93 13.27 2.34 21.06-1.5 7.24-5.17 15.11-11.32 23.16l-151.94 178.1v170.76h-12.34zm95.61-347.71-69.16 81.05-18.67-16.01 69.16-81.05 18.67 16.01zm-84.8 99.39-24.45 28.66-18.68-16.01 24.45-28.66 18.68 16.01zM189.64 386.06h133.64V235.48l3-8L480.45 46.79c3.77-4.97 5.94-9.39 6.7-13.04.45-2.2.35-3.95-.24-5.12-.49-.97-1.58-1.87-3.19-2.55-2.53-1.13-6.06-1.64-10.44-1.42l-439.84.06c-3.33-.05-5.83.41-7.5 1.18-.68.32-1.09.65-1.18.92-.32.91-.2 2.48.33 4.46 1.05 3.96 3.61 8.57 7.38 13.28L186.7 227.59l2.94 7.89v150.58z"/></svg>

BIN
dashboard/src/assets/filter-outline-new.png


+ 2 - 2
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/DeployEventCard.tsx

@@ -117,7 +117,7 @@ const DeployEventCard: React.FC<Props> = ({ event, appData }) => {
             </ServiceStatusContainer>
             <Spacer inline x={1} />
             <ServiceStatusContainer>
-              <Icon height="16px" src={getStatusIcon(serviceStatus[key])} />
+              <Icon height="12px" src={getStatusIcon(serviceStatus[key])} />
               <Spacer inline x={0.5} />
               <Text color="helper">{serviceStatus[key] === "PROGRESSING" ? "DEPLOYING" : serviceStatus[key]}</Text>
             </ServiceStatusContainer>
@@ -138,7 +138,7 @@ const DeployEventCard: React.FC<Props> = ({ event, appData }) => {
       <Spacer y={0.5} />
       <Container row spaced>
         <Container row>
-          <Icon height="16px" src={getStatusIcon(event.status)} />
+          <Icon height="12px" src={getStatusIcon(event.status)} />
           <Spacer inline width="10px" />
           {renderStatusText()}
           {event.metadata.service_status != null &&

+ 3 - 2
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/utils.ts

@@ -1,6 +1,7 @@
 import healthy from "assets/status-healthy.png";
-import failure from "assets/failure.png";
+import failure from "assets/failure.svg";
 import loading from "assets/loading.gif";
+import canceled from "assets/canceled.svg"
 import api from "shared/api";
 import { PorterAppEvent } from "./types";
 
@@ -45,7 +46,7 @@ export const getStatusIcon = (status: string) => {
         case "PROGRESSING":
             return loading;
         case "CANCELED":
-            return failure;
+            return canceled;
         default:
             return loading;
     }

+ 85 - 0
dashboard/src/main/home/app-dashboard/expanded-app/logs/LogFilter.tsx

@@ -0,0 +1,85 @@
+import React, { useEffect, useState, useRef } from "react";
+
+import styled from "styled-components";
+import arrow from "assets/arrow-down.svg";
+import filterOutline from "assets/filter-outline-icon.svg";
+import filterOutlineWhite from "assets/filter-outline-white.svg";
+import { GenericLogFilter, LogFilterName } from "./types";
+import Icon from "components/porter/Icon";
+import Spacer from "components/porter/Spacer";
+import LogFilterComponent from "./LogFilterComponent";
+
+type Props = {
+    icon?: any;
+    options: { value: any; label: string }[];
+    selected: any;
+    setSelected: any;
+    noMargin?: boolean;
+    dropdownAlignRight?: boolean;
+    filters: GenericLogFilter[];
+    selectedFilterValues: Record<LogFilterName, string>;
+};
+
+const LogFilter: React.FC<Props> = (props) => {
+
+    const getIcon = () => {
+        if (props.filters.every((filter) => GenericLogFilter.isDefault(filter, props.selectedFilterValues[filter.name]))) {
+            return filterOutline;
+        }
+        return filterOutlineWhite;
+    }
+
+    const renderFilters = () => {
+        return props.filters.map((filter, i) => {
+            return <>
+                <LogFilterComponent
+                    key={i}
+                    options={[filter.default, ...filter.options]}
+                    name={filter.displayName}
+                />
+                <Spacer inline x={0.5} />
+            </>
+        })
+    }
+
+    return (
+        <Relative>
+            <StyledRadioFilter>
+                <Icon src={getIcon()} height={"16px"} />
+                <Spacer inline x={1} />
+                <Bar />
+                <Spacer inline x={1} />
+                {renderFilters()}
+            </StyledRadioFilter>
+            {/* {renderDropdown()} */}
+        </Relative>
+    );
+};
+
+export default LogFilter;
+
+const Bar = styled.div`
+  width: 1px;
+  height: calc(18px);
+  background: #494b4f;
+`;
+
+const Relative = styled.div`
+  position: relative;
+`;
+
+const StyledRadioFilter = styled.div<{ noMargin?: boolean }>`
+  height: 40px;
+  font-size: 13px;
+  position: relative;
+  padding: 10px;
+  background: ${(props) => props.theme.fg};
+  border-radius: 5px;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+`;

+ 45 - 0
dashboard/src/main/home/app-dashboard/expanded-app/logs/LogFilterComponent.tsx

@@ -0,0 +1,45 @@
+import Text from "components/porter/Text";
+import React, { useEffect } from "react";
+import styled from "styled-components";
+import { GenericFilterOption } from "./types";
+import Spacer from "components/porter/Spacer";
+import Select from "components/porter/Select";
+
+type Props = {
+    name: string;
+    options: GenericFilterOption[];
+};
+
+const LogFilterComponent: React.FC<Props> = ({
+    options,
+    name,
+}) => {
+    useEffect(() => {
+        // Do something
+    }, []);
+
+    return (
+        <StyledLogFilterComponent>
+            <Text>{name}</Text>
+            <Spacer inline x={0.5} />
+            <Select options={options} height={"30px"} />
+        </StyledLogFilterComponent>
+    );
+};
+
+export default LogFilterComponent;
+
+const StyledLogFilterComponent = styled.div`
+    display: flex;
+    align-items: center;
+    width: 100%;
+    animation: fadeIn 0.3s 0s;
+    @keyframes fadeIn {
+    from {
+        opacity: 0;
+    }
+    to {
+        opacity: 1;
+    }
+    }
+`;

+ 78 - 103
dashboard/src/main/home/app-dashboard/expanded-app/logs/LogSection.tsx

@@ -7,7 +7,6 @@ import React, {
 } from "react";
 
 import styled from "styled-components";
-import RadioFilter from "components/RadioFilter";
 
 import spinner from "assets/loading.gif";
 import filterOutline from "assets/filter-outline.svg";
@@ -15,8 +14,7 @@ import filterOutlineWhite from "assets/filter-outline-white.svg";
 import { Context } from "shared/Context";
 import api from "shared/api";
 import { useLogs } from "./utils";
-import { Direction } from "./types";
-import Anser from "anser";
+import { Direction, GenericFilterOption, GenericLogFilter, LogFilterName } from "./types";
 import dayjs, { Dayjs } from "dayjs";
 import Loading from "components/Loading";
 import _ from "lodash";
@@ -30,6 +28,8 @@ import Spacer from "components/porter/Spacer";
 import Container from "components/porter/Container";
 import Button from "components/porter/Button";
 import { Service } from "../../new-app-flow/serviceTypes";
+import LogFilter from "./LogFilter";
+import StyledLogs from "./StyledLogs";
 
 type Props = {
   currentChart?: ChartType;
@@ -69,6 +69,64 @@ const LogSection: React.FC<Props> = ({
   const [isPorterAgentInstalling, setIsPorterAgentInstalling] = useState(false);
   const [isLoading, setIsLoading] = useState(true);
   const [logsError, setLogsError] = useState<string | undefined>(undefined);
+  const [selectedFilterValues, setSelectedFilterValues] = useState<Record<LogFilterName, string>>({
+    revision: "all",
+    output_stream: "all",
+    pod_name: "all",
+  });
+
+  const createVersionOptions = (number: number) => {
+    return Array.from({ length: number }, (_, index) => {
+      const version = index + 1;
+      const label = `Version ${version}`;
+      const value = version.toString();
+      return GenericFilterOption.of(label, value);
+    }).reverse().slice(0, 3);
+  }
+
+  const [filters, setFilters] = useState<GenericLogFilter[]>([
+    {
+      name: "pod_name",
+      displayName: "Service",
+      default: GenericFilterOption.of("All", "all"),
+      options: services?.map(s => {
+        return GenericFilterOption.of(s.name, `${currentChart?.name}-${s.name}-${s.type == "worker" ? "wkr" : s.type}`)
+      }) ?? [],
+      setOption: (option: GenericFilterOption) => {
+        setSelectedFilterValues((s) => ({
+          ...s,
+          pod_name: option.value,
+        }));
+      }
+    },
+    {
+      name: "revision",
+      displayName: "Version",
+      default: GenericFilterOption.of("All", "all"),
+      options: currentChart != null ? createVersionOptions(currentChart.version) : [],
+      setOption: (option: GenericFilterOption) => {
+        setSelectedFilterValues((s) => ({
+          ...s,
+          revision: option.value,
+        }));
+      }
+    },
+    {
+      name: "output_stream",
+      displayName: "Output Stream",
+      default: GenericFilterOption.of("All", "all"),
+      options: [
+        GenericFilterOption.of("stdout", "stdout"),
+        GenericFilterOption.of("stderr", "stderr"),
+      ],
+      setOption: (option: GenericFilterOption) => {
+        setSelectedFilterValues((s) => ({
+          ...s,
+          output_stream: option.value,
+        }));
+      }
+    },
+  ]);
 
   const notify = (message: string) => {
     setNotification(message);
@@ -116,13 +174,6 @@ const LogSection: React.FC<Props> = ({
     }
   }, [isLoading, logs, scrollToBottomRef, scrollToBottomEnabled]);
 
-  useEffect(() => {
-    if (podFilter.podName != "") {
-      setSelectedDateIfUndefined();
-      return;
-    }
-  }, [podFilter]);
-
   useEffect(() => {
     if (selectedDate == null) {
       resetPodFilter();
@@ -136,37 +187,6 @@ const LogSection: React.FC<Props> = ({
     }
   };
 
-  const renderLogs = () => {
-    return logs?.map((log, i) => {
-      return (
-        <Log key={[log.lineNumber, i].join(".")}>
-          <span className="line-number">{log.lineNumber}.</span>
-          <span className="line-timestamp">
-            {log.timestamp
-              ? dayjs(log.timestamp).format("MMM D, YYYY HH:mm:ss")
-              : "-"}
-          </span>
-          <LogOuter key={[log.lineNumber, i].join(".")}>
-            {log.line?.map((ansi, j) => {
-              if (ansi.clearLine) {
-                return null;
-              }
-
-              return (
-                <LogInnerSpan
-                  key={[log.lineNumber, i, j].join(".")}
-                  ansi={ansi}
-                >
-                  {ansi.content.replace(/ /g, "\u00a0")}
-                </LogInnerSpan>
-              );
-            })}
-          </LogOuter>
-        </Log>
-      );
-    });
-  };
-
   const setPodFilterWithPodName = (podName: string) => {
     if (podName == "All") {
       resetPodFilter();
@@ -225,17 +245,6 @@ const LogSection: React.FC<Props> = ({
               setSelectedDate={setSelectedDate}
               resetSearch={resetSearch}
             />
-            {showFilter &&
-              <RadioFilter
-                icon={
-                  podFilter.podName == "" ? filterOutline : filterOutlineWhite
-                }
-                selected={podFilter.podName}
-                setSelected={setPodFilterWithPodName}
-                options={radioOptions}
-                name="Filter logs"
-              />
-            }
           </Flex>
           <Flex>
             <ScrollButton onClick={() => setScrollToBottomEnabled((s) => !s)}>
@@ -256,6 +265,22 @@ const LogSection: React.FC<Props> = ({
             </ScrollButton>
           </Flex>
         </FlexRow>
+        <Spacer y={0.5} />
+        {showFilter &&
+          <>
+            <LogFilter
+              icon={
+                podFilter.podName === "" ? filterOutline : filterOutlineWhite
+              }
+              selected={podFilter.podName}
+              setSelected={setPodFilterWithPodName}
+              options={radioOptions}
+              filters={filters}
+              selectedFilterValues={selectedFilterValues}
+            />
+            <Spacer y={0.5} />
+          </>
+        }
         <LogsSectionWrapper>
           <StyledLogsSection>
             {isLoading || (logs.length == 0 && selectedDate == null) ? (
@@ -281,7 +306,7 @@ const LogSection: React.FC<Props> = ({
                 >
                   Load Previous
                 </LoadMoreButton>
-                {renderLogs()}
+                <StyledLogs logs={logs} />
                 <LoadMoreButton
                   active={selectedDate && logs.length !== 0}
                   role="button"
@@ -480,7 +505,6 @@ const ScrollButton = styled.div`
 const Flex = styled.div`
   display: flex;
   align-items: center;
-  border-bottom: 25px solid transparent;
 `;
 
 const Message = styled.div`
@@ -544,55 +568,6 @@ const StyledLogsSection = styled.div`
   }
 `;
 
-const Log = styled.div`
-  font-family: monospace;
-  user-select: text;
-  display: flex;
-  align-items: flex-end;
-  gap: 8px;
-  width: 100%;
-  & > * {
-    padding-block: 5px;
-  }
-  & > .line-timestamp {
-    height: 100%;
-    color: #949effff;
-    opacity: 0.5;
-    font-family: monospace;
-    min-width: fit-content;
-    padding-inline-end: 5px;
-  }
-  & > .line-number {
-    height: 100%;
-    background: #202538;
-    display: inline-block;
-    text-align: right;
-    min-width: 45px;
-    padding-inline-end: 5px;
-    opacity: 0.3;
-    font-family: monospace;
-  }
-`;
-
-const LogOuter = styled.div`
-  display: inline-block;
-  word-wrap: anywhere;
-  flex-grow: 1;
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-`;
-
-const LogInnerSpan = styled.span`
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-  font-weight: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.decoration && props.ansi?.decoration == "bold" ? "700" : "400"};
-  color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.fg ? `rgb(${props.ansi?.fg})` : "white"};
-  background-color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.bg ? `rgb(${props.ansi?.bg})` : "transparent"};
-`;
-
 const LoadMoreButton = styled.div<{ active: boolean }>`
   width: 100%;
   display: ${(props) => (props.active ? "flex" : "none")};

+ 118 - 0
dashboard/src/main/home/app-dashboard/expanded-app/logs/StyledLogs.tsx

@@ -0,0 +1,118 @@
+import React from "react";
+import { PorterLog } from "./types";
+import styled from "styled-components";
+import Anser from "anser";
+import dayjs from "dayjs";
+
+
+type Props = {
+    logs: PorterLog[];
+};
+
+const StyledLogs: React.FC<Props> = ({
+    logs
+}) => {
+    return (
+        <>
+            {logs.map((log, i) => {
+                return (
+                    <Log key={[log.lineNumber, i].join(".")}>
+                        <span className="line-number">{log.lineNumber}.</span>
+                        <span className="line-timestamp">
+                            {log.timestamp
+                                ? dayjs(log.timestamp).format("MM/DD/YYYY HH:mm:ss")
+                                : "-"}
+                        </span>
+                        {log.metadata != null &&
+                            <LogInnerPill>
+                                {`Version: ${log.metadata.revision}`}
+                            </LogInnerPill>
+                        }
+                        <LogOuter key={[log.lineNumber, i].join(".")}>
+                            {log.line?.map((ansi, j) => {
+                                if (ansi.clearLine) {
+                                    return null;
+                                }
+
+                                return (
+                                    <LogInnerSpan
+                                        key={[log.lineNumber, i, j].join(".")}
+                                        ansi={ansi}
+                                    >
+                                        {ansi.content.replace(/ /g, "\u00a0")}
+                                    </LogInnerSpan>
+                                );
+                            })}
+                        </LogOuter>
+                    </Log>
+                );
+            })}
+        </>
+    );
+};
+
+export default StyledLogs;
+
+const Log = styled.div`
+  font-family: monospace;
+  user-select: text;
+  display: flex;
+  align-items: flex-end;
+  gap: 8px;
+  width: 100%;
+  & > * {
+    padding-block: 5px;
+  }
+  & > .line-timestamp {
+    height: 100%;
+    color: #949effff;
+    opacity: 0.5;
+    font-family: monospace;
+    min-width: fit-content;
+    padding-inline-end: 5px;
+  }
+  & > .line-number {
+    height: 100%;
+    background: #202538;
+    display: inline-block;
+    text-align: right;
+    min-width: 45px;
+    padding-inline-end: 5px;
+    opacity: 0.3;
+    font-family: monospace;
+  }
+`;
+
+const LogInnerPill = styled.div`
+    display: flex;
+    align-items: center;
+    padding: 0px 5px;
+    height: 90%;
+    color: black;
+    background-color: #949fffff;
+    border-radius: 5px;
+    opacity: 1;
+    font-family: monospace;
+    min-width: fit-content;
+    padding-inline-end: 5px;
+
+`
+
+const LogOuter = styled.div`
+  display: inline-block;
+  word-wrap: anywhere;
+  flex-grow: 1;
+  font-family: monospace, sans-serif;
+  font-size: 12px;
+`;
+
+const LogInnerSpan = styled.span`
+  font-family: monospace, sans-serif;
+  font-size: 12px;
+  font-weight: ${(props: { ansi: Anser.AnserJsonEntry }) =>
+        props.ansi?.decoration && props.ansi?.decoration == "bold" ? "700" : "400"};
+  color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
+        props.ansi?.fg ? `rgb(${props.ansi?.fg})` : "white"};
+  background-color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
+        props.ansi?.bg ? `rgb(${props.ansi?.bg})` : "transparent"};
+`;

+ 25 - 1
dashboard/src/main/home/app-dashboard/expanded-app/logs/types.ts

@@ -10,6 +10,7 @@ export interface PorterLog {
     line: AnserJsonEntry[];
     lineNumber: number;
     timestamp?: string;
+    metadata?: z.infer<typeof AgentLogMetadataSchema>;
 }
 
 export interface PaginationInfo {
@@ -30,4 +31,27 @@ export const AgentLogSchema = z.object({
     timestamp: z.string(),
     metadata: AgentLogMetadataSchema.optional(),
 });
-export type AgentLog = z.infer<typeof AgentLogSchema>;
+export type AgentLog = z.infer<typeof AgentLogSchema>;
+
+export interface GenericFilterOption {
+    label: string;
+    value: string;
+}
+export const GenericFilterOption = {
+    of: (label: string, value: string): GenericFilterOption => {
+        return { label, value };
+    }
+}
+export type LogFilterName = 'revision' | 'output_stream' | 'pod_name';
+export interface GenericLogFilter {
+    name: LogFilterName;
+    displayName: string;
+    default: GenericFilterOption;
+    options: GenericFilterOption[];
+    setOption: (option: GenericFilterOption) => void;
+}
+export const GenericLogFilter = {
+    isDefault: (filter: GenericLogFilter, value: string) => {
+        return filter.default.value === value;
+    }
+}

+ 1 - 1
dashboard/src/main/home/app-dashboard/expanded-app/logs/utils.ts

@@ -23,6 +23,7 @@ export const parseLogs = (logs: any[] = []): PorterLog[] => {
         line: ansiLog,
         lineNumber: idx + 1,
         timestamp: parsed.timestamp,
+        metadata: parsed.metadata,
       };
     } catch (err) {
       return {
@@ -80,7 +81,6 @@ export const useLogs = (
   const {
     newWebsocket,
     openWebsocket,
-    closeWebsocket,
     closeAllWebsockets,
   } = useWebsockets();
 

+ 1 - 1
internal/repository/gorm/porter_app_event.go

@@ -120,7 +120,7 @@ func (repo *PorterAppEventRepository) ReadDeployEventByRevision(ctx context.Cont
 	}
 	strRevision := string(revJSON)
 
-	if err := repo.db.Where("porter_app_id = ? AND metadata->>'revision' = ?", strAppID, strRevision).First(&appEvent).Error; err != nil {
+	if err := repo.db.Where("porter_app_id = ? AND type = 'DEPLOY' AND metadata->>'revision' = ?", strAppID, strRevision).First(&appEvent).Error; err != nil {
 		return appEvent, err
 	}