From 87439ff42fc0cc09ed76332fba8fd36dd728dab9 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Tue, 19 Dec 2023 13:05:05 -0500 Subject: [PATCH 1/7] wip --- api/server/handlers/porter_app/service_status.go | 7 +------ dashboard/src/lib/hooks/useAppStatus.ts | 6 +----- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/api/server/handlers/porter_app/service_status.go b/api/server/handlers/porter_app/service_status.go index f1e1f54a67..f2435d19ee 100644 --- a/api/server/handlers/porter_app/service_status.go +++ b/api/server/handlers/porter_app/service_status.go @@ -137,13 +137,8 @@ func (c *ServiceStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) return } - appRevisions := listAppRevisionsResp.Msg.AppRevisions - if appRevisions == nil { - appRevisions = []*porterv1.AppRevision{} - } - var revisions []porter_app.Revision - for _, revision := range appRevisions { + for _, revision := range listAppRevisionsResp.Msg.AppRevisions { encodedRevision, err := porter_app.EncodedRevisionFromProto(ctx, revision) if err != nil { err := telemetry.Error(ctx, span, err, "error getting encoded revision from proto") diff --git a/dashboard/src/lib/hooks/useAppStatus.ts b/dashboard/src/lib/hooks/useAppStatus.ts index 2a09f5b62d..caef0a481f 100644 --- a/dashboard/src/lib/hooks/useAppStatus.ts +++ b/dashboard/src/lib/hooks/useAppStatus.ts @@ -26,11 +26,7 @@ const serviceStatusValidator = z.object({ revision_number: z.number(), instance_status_list: z.array( z.object({ - status: z.union([ - z.literal("PENDING"), - z.literal("RUNNING"), - z.literal("FAILED"), - ]), + status: z.enum(["PENDING", "RUNNING", "FAILED"]), restart_count: z.number(), creation_timestamp: z.string(), }) From f4e3227679f4130cb7c497912bee0a551c4345d7 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Fri, 22 Dec 2023 11:52:07 -0500 Subject: [PATCH 2/7] wip --- dashboard/src/lib/hooks/useAppStatus.ts | 18 +- .../app-view/AppDataContainer.tsx | 6 +- .../app-dashboard/app-view/tabs/StatusTab.tsx | 188 +++++++++ .../app-view/tabs/status/ServiceStatus.tsx | 389 ++++++++++++++++++ .../status/ServiceVersionInstanceStatus.tsx | 187 +++++++++ .../tabs/status/ServiceVersionStatus.tsx | 7 + .../services-settings/ServiceContainer.tsx | 6 +- 7 files changed, 790 insertions(+), 11 deletions(-) create mode 100644 dashboard/src/main/home/app-dashboard/app-view/tabs/StatusTab.tsx create mode 100644 dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceStatus.tsx create mode 100644 dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx create mode 100644 dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionStatus.tsx diff --git a/dashboard/src/lib/hooks/useAppStatus.ts b/dashboard/src/lib/hooks/useAppStatus.ts index caef0a481f..86be4c63fe 100644 --- a/dashboard/src/lib/hooks/useAppStatus.ts +++ b/dashboard/src/lib/hooks/useAppStatus.ts @@ -14,8 +14,9 @@ export type ClientServiceStatus = { status: "running" | "spinningDown" | "failing"; message: string; crashLoopReason: string; - restartCount?: number; + restartCount: number; revisionId: string; + revisionNumber: number; }; const serviceStatusValidator = z.object({ @@ -146,9 +147,10 @@ export const useAppStatus = ({ revisionStatus.revision_number }`, crashLoopReason: "", - restartCount: _.maxBy(runningInstances, "restart_count") - ?.restart_count, + restartCount: + _.maxBy(runningInstances, "restart_count")?.restart_count ?? 0, revisionId: revisionStatus.revision_id, + revisionNumber: revisionStatus.revision_number, }); } if (pendingInstances.length > 0) { @@ -162,9 +164,10 @@ export const useAppStatus = ({ pendingInstances.length )} in a pending state at Version ${revisionStatus.revision_number}`, crashLoopReason: "", - restartCount: _.maxBy(pendingInstances, "restart_count") - ?.restart_count, + restartCount: + _.maxBy(pendingInstances, "restart_count")?.restart_count ?? 0, revisionId: revisionStatus.revision_id, + revisionNumber: revisionStatus.revision_number, }); } if (failedInstances.length > 0) { @@ -178,9 +181,10 @@ export const useAppStatus = ({ failedInstances.length )} failing to run Version ${revisionStatus.revision_number}`, crashLoopReason: "", - restartCount: _.maxBy(failedInstances, "restart_count") - ?.restart_count, + restartCount: + _.maxBy(failedInstances, "restart_count")?.restart_count ?? 0, revisionId: revisionStatus.revision_id, + revisionNumber: revisionStatus.revision_number, }); } return versionStatuses; diff --git a/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx b/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx index 44b1cf9e0a..1186dc97c2 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx @@ -60,6 +60,7 @@ import MetricsTab from "./tabs/MetricsTab"; import Notifications from "./tabs/Notifications"; import Overview from "./tabs/Overview"; import Settings from "./tabs/Settings"; +import StatusTab from "./tabs/StatusTab"; // commented out tabs are not yet implemented // will be included as support is available based on data from app revisions rather than helm releases @@ -78,6 +79,7 @@ const validTabs = [ "helm-values", "job-history", "notifications", + "status", ] as const; const DEFAULT_TAB = "activity"; type ValidTab = (typeof validTabs)[number]; @@ -505,9 +507,10 @@ const AppDataContainer: React.FC = ({ tabParam }) => { ) : undefined, }, { label: "Activity", value: "activity" }, - { label: "Overview", value: "overview" }, + { label: "Status", value: "status" }, { label: "Logs", value: "logs" }, { label: "Metrics", value: "metrics" }, + { label: "Services", value: "overview" }, { label: "Environment", value: "environment" }, ]; @@ -665,6 +668,7 @@ const AppDataContainer: React.FC = ({ tabParam }) => { /> )) .with("notifications", () => ) + .with("status", () => ) .otherwise(() => null)} diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/StatusTab.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/StatusTab.tsx new file mode 100644 index 0000000000..1cff6a7489 --- /dev/null +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/StatusTab.tsx @@ -0,0 +1,188 @@ +import React from "react"; +import styled from "styled-components"; + +import Loading from "components/Loading"; +import { useAppStatus } from "lib/hooks/useAppStatus"; + +import { valueExists } from "shared/util"; + +import { useLatestRevision } from "../LatestRevisionContext"; +import ServiceStatus from "./status/ServiceStatus"; + +const StatusTab: React.FC = () => { + const { + projectId, + clusterId, + latestClientServices, + deploymentTarget, + appName, + } = useLatestRevision(); + + const { serviceVersionStatus } = useAppStatus({ + projectId, + clusterId, + serviceNames: latestClientServices.map((s) => s.name.value), + deploymentTargetId: deploymentTarget.id, + appName, + }); + + // const renderLogs = () => { + // return ( + // + // ); + // }; + + // const renderTabs = () => { + // return controllers.map((c, i) => { + // return ( + // setPodError(x)} + // /> + // ); + // }); + // }; + + const renderStatusSection = (): JSX.Element => { + if (Object.keys(serviceVersionStatus).length === 0) { + return ( + + + + ); + } + + return ( + + {Object.keys(serviceVersionStatus) + .map((serviceName, i) => { + const serviceStatus = serviceVersionStatus[serviceName]; + const clientService = latestClientServices.find( + (s) => s.name.value === serviceName + ); + if (clientService) { + return ( + + ); + } + return null; + }) + .filter(valueExists)} + + ); + // if (controllers?.length > 0) { + // return ( + // + // {renderTabs()} + // {renderLogs()} + // + // ); + // } + + // if (currentChart?.chart?.metadata?.name === "job") { + // return ( + // + // category + // There are no jobs currently running. + // + // ); + // } + + // return ( + // + // category + // No objects to display. This might happen while your app is still + // deploying. + // + // ); + }; + + return {renderStatusSection()}; +}; + +export default StatusTab; + +const TabWrapper = styled.div` + width: 35%; + min-width: 250px; + height: 100%; + overflow-y: auto; +`; + +const StyledStatusSection = styled.div` + padding: 0px; + user-select: text; + overflow: hidden; + width: 100%; + min-height: 400px; + height: calc(100vh - 400px); + font-size: 13px; + overflow: hidden; + border-radius: 8px; + animation: floatIn 0.3s; + animation-timing-function: ease-out; + animation-fill-mode: forwards; + @keyframes floatIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0px); + } + } +`; + +const FullScreen = styled.div` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + padding-top: 60px; +`; + +const Wrapper = styled.div` + width: 100%; + height: 100%; + display: flex; +`; + +const NoControllers = styled.div` + padding-top: 20%; + position: relative; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + color: #ffffff44; + font-size: 14px; + + > i { + font-size: 18px; + margin-right: 12px; + } +`; + +const ServiceVersionContainer = styled.div` + display: flex; + flex-direction: column; + gap: 10px; +`; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceStatus.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceStatus.tsx new file mode 100644 index 0000000000..22dfdf58e6 --- /dev/null +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceStatus.tsx @@ -0,0 +1,389 @@ +import React, { useState } from "react"; +import styled from "styled-components"; + +import Icon from "components/porter/Icon"; +import Spacer from "components/porter/Spacer"; +import { type ClientServiceStatus } from "lib/hooks/useAppStatus"; +import { type ClientService } from "lib/porter-apps/services"; + +import job from "assets/job.png"; +import web from "assets/web.png"; +import worker from "assets/worker.png"; + +import ServiceVersionInstanceStatus from "./ServiceVersionInstanceStatus"; + +type Props = { + isLast: boolean; + service: ClientService; + serviceVersionStatusList: ClientServiceStatus[]; +}; + +const ServiceStatus: React.FC = ({ + isLast, + serviceVersionStatusList, + service, +}) => { + const [expanded, setExpanded] = useState(false); + + const renderIcon = (service: ClientService): JSX.Element => { + switch (service.config.type) { + case "web": + return ; + case "worker": + return ; + case "job": + return ; + case "predeploy": + return ; + } + }; + + return ( + ({})} + roundAllCorners={true} + > + {/* { + setExpanded(!expanded); + }} + > + + + arrow_right + + + {match(service.config.type) + .with("web", () => ) + .with("worker", () => ) + .with("job", () => ) + .with("predeploy", () => ) + .exhaustive()} + + {service.name.value} + + + + test status + + + */} + { + setExpanded(!expanded); + }} + bordersRounded={!expanded} + > + + + arrow_drop_down + + {renderIcon(service)} + + {service.name.value} + + + + test status + + + + {expanded && ( + + {serviceVersionStatusList.map((versionStatus) => { + return ( +
+ { + setExpanded(!expanded); + }} + > + + + arrow_right + + + Version {versionStatus.revisionNumber}: + + + + test status + + + + {/* + + Version {versionStatus.revisionNumber}: + + */} + +
+ ); + })} + {/* handleDeletePod(podPendingDelete)} + onNo={() => setPodPendingDelete(null)} + /> */} +
+ )} +
+ ); +}; + +export default ServiceStatus; + +const StyledResourceTab = styled.div` + width: 100%; + margin-bottom: 2px; + font-size: 13px; + background: ${(props) => props.theme.fg}; + border-bottom-left-radius: ${(props: { + isLast: boolean; + roundAllCorners: boolean; + }) => (props.isLast ? "10px" : "")}; +`; + +// const Tooltip = styled.div` +// position: absolute; +// right: 0px; +// top: 25px; +// white-space: nowrap; +// height: 18px; +// padding: 2px 5px; +// background: #383842dd; +// display: flex; +// align-items: center; +// justify-content: center; +// flex: 1; +// color: white; +// text-transform: none; +// font-size: 12px; +// outline: 1px solid #ffffff55; +// opacity: 0; +// animation: faded-in 0.2s 0.15s; +// animation-fill-mode: forwards; +// @keyframes faded-in { +// from { +// opacity: 0; +// } +// to { +// opacity: 1; +// } +// } +// `; + +const ExpandWrapper = styled.div``; + +const ResourceHeader = styled.div` + width: 100%; + height: 50px; + display: flex; + font-size: 13px; + align-items: center; + justify-content: space-between; + user-select: none; + padding: 8px 18px; + padding-left: ${(props: { expanded: boolean; hasChildren: boolean }) => + props.hasChildren ? "10px" : "22px"}; + cursor: pointer; + background: ${(props: { expanded: boolean; hasChildren: boolean }) => + props.expanded ? "#ffffff11" : ""}; + :hover { + background: #ffffff18; + + > i { + background: #ffffff22; + } + } +`; + +const Info = styled.div` + display: flex; + flex-direction: row; + align-items: center; + width: 80%; + height: 100%; +`; + +const Metadata = styled.div` + display: flex; + align-items: center; + position: relative; + max-width: ${(props: { hasStatus: boolean }) => + props.hasStatus ? "calc(100% - 20px)" : "100%"}; +`; + +const Status = styled.div` + display: flex; + width; 20%; + font-size: 12px; + text-transform: capitalize; + justify-content: flex-end; + align-items: center; + color: #aaaabb; + animation: fadeIn 0.5s; + @keyframes fadeIn { + from { opacity: 0 } + to { opacity: 1 } + } +`; + +const StatusColor = styled.div` + margin-left: 12px; + width: 8px; + min-width: 8px; + height: 8px; + background: ${(props: { status: string }) => + props.status === "running" || + props.status === "Ready" || + props.status === "Completed" + ? "#4797ff" + : props.status === "failed" || props.status === "FailedValidation" + ? "#ed5f85" + : props.status === "completed" + ? "#00d12a" + : "#f5cb42"}; + border-radius: 20px; +`; + +const ResourceName = styled.div` + color: #ffffff; + max-width: 40%; + margin-left: ${(props: { showKindLabels: boolean }) => + props.showKindLabels ? "10px" : ""}; + text-transform: none; + white-space: nowrap; +`; + +const IconWrapper = styled.div` + width: 25px; + height: 25px; + display: flex; + align-items: center; + justify-content: center; + + > i { + font-size: 15px; + color: #ffffff; + margin-right: 14px; + } +`; + +const DropdownIcon = styled.div` + > i { + margin-top: 2px; + margin-right: 11px; + font-size: 20px; + color: #ffffff66; + cursor: pointer; + border-radius: 20px; + background: ${(props: { expanded: boolean }) => + props.expanded ? "#ffffff18" : ""}; + transform: ${(props: { expanded: boolean }) => + props.expanded ? "rotate(180deg)" : ""}; + animation: ${(props: { expanded: boolean }) => + props.expanded ? "quarterTurn 0.3s" : ""}; + animation-fill-mode: forwards; + + @keyframes quarterTurn { + from { + transform: rotate(0deg); + } + to { + transform: rotate(90deg); + } + } + } +`; + +const ReplicaSetContainer = styled.div` + padding: 10px 5px; + display: flex; + overflow-wrap: anywhere; + justify-content: space-between; + border-top: 2px solid #ffffff11; +`; + +const ReplicaSetName = styled.span` + padding-left: 10px; + overflow-wrap: anywhere; + max-width: calc(100% - 45px); + line-height: 1.5em; + color: #ffffff33; +`; + +const Bold = styled.span` + font-weight: 500; + display: inline; + color: #ffffff; +`; + +const ActionButton = styled.button` + position: relative; + border: none; + background: none; + color: white; + padding: 5px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + cursor: pointer; + color: #aaaabb; + + > span { + font-size: 20px; + } + margin-right: 5px; +`; + +const ServiceHeader = styled.div<{ + showExpanded?: boolean; + bordersRounded?: boolean; +}>` + flex-direction: row; + display: flex; + height: 60px; + font-size: 18px; + justify-content: space-between; + cursor: pointer; + padding: 20px; + color: ${(props) => props.theme.text.primary}; + position: relative; + border-radius: 5px; + background: ${(props) => props.theme.clickable.bg}; + border: 1px solid #494b4f; + :hover { + border: 1px solid #7a7b80; + ${ActionButton} { + color: white; + } + } + + border-bottom-left-radius: ${(props) => (props.bordersRounded ? "" : "0")}; + border-bottom-right-radius: ${(props) => (props.bordersRounded ? "" : "0")}; + + .dropdown { + font-size: 30px; + cursor: pointer; + border-radius: 20px; + margin-left: -10px; + transform: ${(props: { showExpanded?: boolean }) => + props.showExpanded ? "" : "rotate(-90deg)"}; + } +`; + +const ServiceTitle = styled.div` + display: flex; + align-items: center; +`; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx new file mode 100644 index 0000000000..24db12be11 --- /dev/null +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx @@ -0,0 +1,187 @@ +import React from "react"; +import styled from "styled-components"; + +import Tooltip from "components/porter/Tooltip"; +import { type ClientServiceStatus } from "lib/hooks/useAppStatus"; + +type Props = { + serviceVersionStatus: ClientServiceStatus; +}; +const ServiceVersionInstanceStatus: React.FC = ({ + serviceVersionStatus, +}) => { + return ( + ({})}> + + + + + + + Version: {serviceVersionStatus.revisionNumber} + Restart count: {serviceVersionStatus.restartCount} + Created on: 3 days + {serviceVersionStatus.status === "failing" ? ( + + + Failure Reason: {serviceVersionStatus.crashLoopReason} + + + ) : null} + + } + > + Version: {serviceVersionStatus.revisionNumber} + + + + {serviceVersionStatus.status} + + + ); +}; + +export default ServiceVersionInstanceStatus; + +const Grey = styled.div` + margin-top: 5px; + color: #aaaabb; +`; + +const FailedStatusContainer = styled.div` + width: 100%; + border: 1px solid hsl(0deg, 100%, 30%); + padding: 5px; + margin-block: 5px; +`; + +const InstanceTooltip = styled.div` + position: absolute; + left: 35px; + word-wrap: break-word; + top: 38px; + min-height: 18px; + max-width: calc(100% - 75px); + padding: 5px 7px; + background: #272731; + z-index: 999; + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; + color: white; + text-transform: none; + font-size: 12px; + font-family: "Work Sans", sans-serif; + outline: 1px solid #ffffff55; + opacity: 0; + animation: faded-in 0.2s 0.15s; + animation-fill-mode: forwards; + @keyframes faded-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } +`; + +const Tab = styled.div` + width: 100%; + height: 50px; + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + color: ${(props: { selected: boolean }) => + props.selected ? "white" : "#ffffff66"}; + background: ${(props: { selected: boolean }) => + props.selected ? "#ffffff18" : ""}; + font-size: 13px; + padding: 20px 19px 20px 42px; + text-shadow: 0px 0px 8px none; + overflow: visible; + cursor: pointer; + :hover { + color: white; + background: #ffffff18; + } +`; + +const Rail = styled.div` + width: 2px; + background: ${(props: { lastTab?: boolean }) => + props.lastTab ? "" : "#52545D"}; + height: 50%; +`; + +const Circle = styled.div` + min-width: 10px; + min-height: 2px; + margin-bottom: -2px; + margin-left: 8px; + background: #52545d; +`; + +const Gutter = styled.div` + position: absolute; + top: 0px; + left: 10px; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + overflow: visible; +`; + +const InstanceStatus = styled.div` + display: flex; + font-size: 12px; + text-transform: capitalize; + margin-left: 5px; + justify-content: flex-end; + align-items: center; + font-family: "Work Sans", sans-serif; + color: #aaaabb; + animation: fadeIn 0.5s; + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } +`; + +const Name = styled.div` + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.5em; + display: -webkit-box; + overflow-wrap: anywhere; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +`; + +const StatusColor = styled.div` + margin-left: 12px; + width: 8px; + min-width: 8px; + height: 8px; + background: ${(props: { status: string }) => + props.status === "running" || + props.status === "Ready" || + props.status === "Completed" + ? "#4797ff" + : props.status === "failed" || props.status === "FailedValidation" + ? "#ed5f85" + : props.status === "completed" + ? "#00d12a" + : "#f5cb42"}; + border-radius: 20px; +`; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionStatus.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionStatus.tsx new file mode 100644 index 0000000000..08f4c2bfb9 --- /dev/null +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionStatus.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const ServiceVersionStatus: React.FC = () => { + return
{/* Your component content goes here */}
; +}; + +export default ServiceVersionStatus; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx index 331c6b3e52..28f476bced 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx @@ -230,9 +230,6 @@ const ActionButton = styled.button` border-radius: 50%; cursor: pointer; color: #aaaabb; - :hover { - color: white; - } > span { font-size: 20px; @@ -257,6 +254,9 @@ const ServiceHeader = styled.div<{ border: 1px solid #494b4f; :hover { border: 1px solid #7a7b80; + ${ActionButton} { + color: white; + } } border-bottom-left-radius: ${(props) => (props.bordersRounded ? "" : "0")}; From c801e5d721641f1beefde09b03f6f349d8704a44 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Wed, 3 Jan 2024 11:20:49 -0500 Subject: [PATCH 3/7] new debug tab --- dashboard/src/components/porter/Tooltip.tsx | 46 ++- dashboard/src/lib/hooks/useAppStatus.ts | 193 ++++++++----- .../app-dashboard/app-view/tabs/StatusTab.tsx | 98 +------ .../app-view/tabs/status/ServiceStatus.tsx | 267 ++---------------- .../status/ServiceVersionInstanceStatus.tsx | 125 ++++---- .../tabs/status/ServiceVersionStatus.tsx | 181 +++++++++++- .../app-view/tabs/status/StatusTags.tsx | 158 +++++++++++ .../services-settings/ServiceContainer.tsx | 4 +- .../services-settings/ServiceList.tsx | 4 +- .../services-settings/ServiceStatusFooter.tsx | 6 +- internal/porter_app/status.go | 5 +- 11 files changed, 611 insertions(+), 476 deletions(-) create mode 100644 dashboard/src/main/home/app-dashboard/app-view/tabs/status/StatusTags.tsx diff --git a/dashboard/src/components/porter/Tooltip.tsx b/dashboard/src/components/porter/Tooltip.tsx index acd9a217f2..9ac9a233ee 100644 --- a/dashboard/src/components/porter/Tooltip.tsx +++ b/dashboard/src/components/porter/Tooltip.tsx @@ -2,34 +2,52 @@ import React, { useState } from "react"; import styled from "styled-components"; -interface TooltipProps { +type TooltipProps = { children: React.ReactNode; content: React.ReactNode; position?: "top" | "right" | "bottom" | "left"; hidden?: boolean; - width?: string; -} + tooltipContentWidth?: string; + backgroundColor?: string; + containerWidth?: string; +}; const Tooltip: React.FC = ({ children, content, position = "top", hidden = false, - width, + backgroundColor = "#333", + tooltipContentWidth, + containerWidth, }) => { const [isVisible, setIsVisible] = useState(false); - const showTooltip = () => setIsVisible(true); - const hideTooltip = () => setIsVisible(false); + const showTooltip = (): void => { + setIsVisible(true); + }; + const hideTooltip = (): void => { + setIsVisible(false); + }; if (hidden) { return <>{children}; } return ( - + {isVisible && ( - {content} + + {content} + )} {children} @@ -38,13 +56,19 @@ const Tooltip: React.FC = ({ export default Tooltip; -const TooltipContainer = styled.div` +const TooltipContainer = styled.div<{ width?: string }>` position: relative; display: inline-flex; + ${({ width }) => width && `width: ${width};`} + height: 100%; `; -const TooltipContent = styled.div<{ position: string, width?: string }>` - background-color: #333; +const TooltipContent = styled.div<{ + position: string; + width?: string; + backgroundColor: string; +}>` + background-color: ${({ backgroundColor }) => backgroundColor}; color: #fff; padding: 8px; border-radius: 4px; diff --git a/dashboard/src/lib/hooks/useAppStatus.ts b/dashboard/src/lib/hooks/useAppStatus.ts index 86be4c63fe..98b59db9c9 100644 --- a/dashboard/src/lib/hooks/useAppStatus.ts +++ b/dashboard/src/lib/hooks/useAppStatus.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import _ from "lodash"; -import pluralize from "pluralize"; +import { match } from "ts-pattern"; import z from "zod"; import api from "shared/api"; @@ -8,15 +8,33 @@ import { useWebsockets, type NewWebsocketOptions, } from "shared/hooks/useWebsockets"; -import { valueExists } from "shared/util"; + +export type ServiceStatusDescriptor = + | "running" + | "pending" + | "failing" + | "unknown"; export type ClientServiceStatus = { - status: "running" | "spinningDown" | "failing"; + status: ServiceStatusDescriptor; + serviceName: string; + versionStatusList: ClientServiceVersionStatus[]; +}; + +export type ClientServiceVersionStatus = { + status: ServiceStatusDescriptor; + revisionId: string; + revisionNumber: number; + instanceStatusList: ClientServiceVersionInstanceStatus[]; +}; + +export type ClientServiceVersionInstanceStatus = { + status: ServiceStatusDescriptor; message: string; crashLoopReason: string; restartCount: number; - revisionId: string; - revisionNumber: number; + name: string; + creationTimestamp: string; }; const serviceStatusValidator = z.object({ @@ -30,6 +48,7 @@ const serviceStatusValidator = z.object({ status: z.enum(["PENDING", "RUNNING", "FAILED"]), restart_count: z.number(), creation_timestamp: z.string(), + name: z.string(), }) ), }) @@ -51,7 +70,9 @@ export const useAppStatus = ({ deploymentTargetId: string; appName: string; kind?: string; -}): { serviceVersionStatus: Record } => { +}): { + appServiceStatus: Record; +} => { const [serviceStatusMap, setServiceStatusMap] = useState< Record >({}); @@ -124,78 +145,108 @@ export const useAppStatus = ({ const deserializeServiceStatus = ( serviceStatus: SerializedServiceStatus - ): ClientServiceStatus[] => { - return serviceStatus.revision_status_list + ): ClientServiceStatus => { + const clientServiceStatus: ClientServiceStatus = { + status: "unknown", + serviceName: serviceStatus.service_name, + versionStatusList: [], + }; + + const versionStatusList = serviceStatus.revision_status_list .sort((a, b) => b.revision_number - a.revision_number) - .flatMap((revisionStatus) => { - const instancesByStatus = _.groupBy( - revisionStatus.instance_status_list, - (instance) => instance.status - ); - const runningInstances = instancesByStatus.RUNNING || []; - const pendingInstances = instancesByStatus.PENDING || []; - const failedInstances = instancesByStatus.FAILED || []; - const versionStatuses: ClientServiceStatus[] = []; - - if (runningInstances.length > 0) { - versionStatuses.push({ - status: "running", - message: `${runningInstances.length} ${pluralize( - "instance", - runningInstances.length - )} ${pluralize("is", runningInstances.length)} running at Version ${ - revisionStatus.revision_number - }`, - crashLoopReason: "", - restartCount: - _.maxBy(runningInstances, "restart_count")?.restart_count ?? 0, - revisionId: revisionStatus.revision_id, - revisionNumber: revisionStatus.revision_number, + .map((revisionStatus) => { + const clientServiceVersionStatus: ClientServiceVersionStatus = { + status: "unknown", + revisionId: revisionStatus.revision_id, + revisionNumber: revisionStatus.revision_number, + instanceStatusList: [], + }; + + const instanceStatusList = revisionStatus.instance_status_list + .sort((a, b) => { + const aDate = new Date(a.creation_timestamp); + const bDate = new Date(b.creation_timestamp); + return bDate.getTime() - aDate.getTime(); + }) + .map((instanceStatus) => { + const status: ServiceStatusDescriptor = match(instanceStatus.status) + .with("PENDING", () => "pending" as const) + .with("RUNNING", () => "running" as const) + .with("FAILED", () => "failing" as const) + .otherwise(() => "unknown" as const); + const clientServiceVersionInstanceStatus: ClientServiceVersionInstanceStatus = + { + status, + message: "", + crashLoopReason: "", + restartCount: instanceStatus.restart_count, + name: instanceStatus.name, + creationTimestamp: instanceStatus.creation_timestamp, + }; + + if (instanceStatus.status === "PENDING") { + clientServiceVersionInstanceStatus.message = `Instance is pending at Version ${revisionStatus.revision_number}`; + } else if (instanceStatus.status === "RUNNING") { + clientServiceVersionInstanceStatus.message = `Instance is running at Version ${revisionStatus.revision_number}`; + } else if (instanceStatus.status === "FAILED") { + clientServiceVersionInstanceStatus.message = `Instance is failing at Version ${revisionStatus.revision_number}`; + } + + return clientServiceVersionInstanceStatus; }); + + clientServiceVersionStatus.instanceStatusList = instanceStatusList; + if ( + instanceStatusList.every((instance) => instance.status === "running") + ) { + clientServiceVersionStatus.status = "running"; } - if (pendingInstances.length > 0) { - versionStatuses.push({ - status: "spinningDown", - message: `${pendingInstances.length} ${pluralize( - "instance", - pendingInstances.length - )} ${pluralize( - "is", - pendingInstances.length - )} in a pending state at Version ${revisionStatus.revision_number}`, - crashLoopReason: "", - restartCount: - _.maxBy(pendingInstances, "restart_count")?.restart_count ?? 0, - revisionId: revisionStatus.revision_id, - revisionNumber: revisionStatus.revision_number, - }); + if ( + instanceStatusList.every((instance) => instance.status === "pending") + ) { + clientServiceVersionStatus.status = "pending"; } - if (failedInstances.length > 0) { - versionStatuses.push({ - status: "failing", - message: `${failedInstances.length} ${pluralize( - "instance", - failedInstances.length - )} ${pluralize( - "is", - failedInstances.length - )} failing to run Version ${revisionStatus.revision_number}`, - crashLoopReason: "", - restartCount: - _.maxBy(failedInstances, "restart_count")?.restart_count ?? 0, - revisionId: revisionStatus.revision_id, - revisionNumber: revisionStatus.revision_number, - }); + if ( + instanceStatusList.every((instance) => instance.status === "failing") + ) { + clientServiceVersionStatus.status = "failing"; } - return versionStatuses; - }) - .filter(valueExists); + + return clientServiceVersionStatus; + }); + + clientServiceStatus.versionStatusList = versionStatusList; + if (versionStatusList.every((version) => version.status === "running")) { + clientServiceStatus.status = "running"; + } + if (versionStatusList.every((version) => version.status === "pending")) { + clientServiceStatus.status = "pending"; + } + if (versionStatusList.every((version) => version.status === "failing")) { + clientServiceStatus.status = "failing"; + } + return clientServiceStatus; }; return { - serviceVersionStatus: _.mapValues( - serviceStatusMap, - deserializeServiceStatus - ), + appServiceStatus: _.mapValues(serviceStatusMap, deserializeServiceStatus), }; }; + +export const statusColor = (status: ServiceStatusDescriptor): string => { + return match(status) + .with("running", () => "#38a88a") + .with("failing", () => "#ff0000") + .with("pending", () => "#FFA500") + .with("unknown", () => "#4797ff") + .exhaustive(); +}; + +export const statusColorLight = (status: ServiceStatusDescriptor): string => { + return match(status) + .with("running", () => "#4b6850") + .with("failing", () => "#FF7F7F") + .with("pending", () => "#FFC04C") + .with("unknown", () => "#e6f2ff") + .exhaustive(); +}; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/StatusTab.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/StatusTab.tsx index 1cff6a7489..d8f78fbc95 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/StatusTab.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/StatusTab.tsx @@ -18,44 +18,18 @@ const StatusTab: React.FC = () => { appName, } = useLatestRevision(); - const { serviceVersionStatus } = useAppStatus({ + const { appServiceStatus } = useAppStatus({ projectId, clusterId, - serviceNames: latestClientServices.map((s) => s.name.value), + serviceNames: latestClientServices + .filter((s) => s.config.type === "web" || s.config.type === "worker") + .map((s) => s.name.value), deploymentTargetId: deploymentTarget.id, appName, }); - // const renderLogs = () => { - // return ( - // - // ); - // }; - - // const renderTabs = () => { - // return controllers.map((c, i) => { - // return ( - // setPodError(x)} - // /> - // ); - // }); - // }; - const renderStatusSection = (): JSX.Element => { - if (Object.keys(serviceVersionStatus).length === 0) { + if (Object.keys(appServiceStatus).length === 0) { return ( @@ -65,18 +39,17 @@ const StatusTab: React.FC = () => { return ( - {Object.keys(serviceVersionStatus) - .map((serviceName, i) => { - const serviceStatus = serviceVersionStatus[serviceName]; + {Object.keys(appServiceStatus) + .map((serviceName) => { + const serviceStatus = appServiceStatus[serviceName]; const clientService = latestClientServices.find( (s) => s.name.value === serviceName ); if (clientService) { return ( ); @@ -86,31 +59,6 @@ const StatusTab: React.FC = () => { .filter(valueExists)} ); - // if (controllers?.length > 0) { - // return ( - // - // {renderTabs()} - // {renderLogs()} - // - // ); - // } - - // if (currentChart?.chart?.metadata?.name === "job") { - // return ( - // - // category - // There are no jobs currently running. - // - // ); - // } - - // return ( - // - // category - // No objects to display. This might happen while your app is still - // deploying. - // - // ); }; return {renderStatusSection()}; @@ -118,22 +66,12 @@ const StatusTab: React.FC = () => { export default StatusTab; -const TabWrapper = styled.div` - width: 35%; - min-width: 250px; - height: 100%; - overflow-y: auto; -`; - const StyledStatusSection = styled.div` padding: 0px; user-select: text; - overflow: hidden; width: 100%; - min-height: 400px; - height: calc(100vh - 400px); + height: 100%; font-size: 13px; - overflow: hidden; border-radius: 8px; animation: floatIn 0.3s; animation-timing-function: ease-out; @@ -150,21 +88,6 @@ const StyledStatusSection = styled.div` } `; -const FullScreen = styled.div` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - padding-top: 60px; -`; - -const Wrapper = styled.div` - width: 100%; - height: 100%; - display: flex; -`; - const NoControllers = styled.div` padding-top: 20%; position: relative; @@ -185,4 +108,5 @@ const ServiceVersionContainer = styled.div` display: flex; flex-direction: column; gap: 10px; + height: 100%; `; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceStatus.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceStatus.tsx index 22dfdf58e6..e626cc1480 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceStatus.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceStatus.tsx @@ -10,19 +10,15 @@ import job from "assets/job.png"; import web from "assets/web.png"; import worker from "assets/worker.png"; -import ServiceVersionInstanceStatus from "./ServiceVersionInstanceStatus"; +import ServiceVersionStatus from "./ServiceVersionStatus"; +import StatusTags from "./StatusTags"; type Props = { - isLast: boolean; service: ClientService; - serviceVersionStatusList: ClientServiceStatus[]; + serviceStatus: ClientServiceStatus; }; -const ServiceStatus: React.FC = ({ - isLast, - serviceVersionStatusList, - service, -}) => { +const ServiceStatus: React.FC = ({ serviceStatus, service }) => { const [expanded, setExpanded] = useState(false); const renderIcon = (service: ClientService): JSX.Element => { @@ -39,38 +35,7 @@ const ServiceStatus: React.FC = ({ }; return ( - ({})} - roundAllCorners={true} - > - {/* { - setExpanded(!expanded); - }} - > - - - arrow_right - - - {match(service.config.type) - .with("web", () => ) - .with("worker", () => ) - .with("job", () => ) - .with("predeploy", () => ) - .exhaustive()} - - {service.name.value} - - - - test status - - - */} + { @@ -87,53 +52,23 @@ const ServiceStatus: React.FC = ({ {service.name.value} - - test status - - + v.instanceStatusList + )} + /> {expanded && ( - {serviceVersionStatusList.map((versionStatus) => { + {serviceStatus.versionStatusList.map((versionStatus) => { return ( -
- { - setExpanded(!expanded); - }} - > - - - arrow_right - - - Version {versionStatus.revisionNumber}: - - - - test status - - - - {/* - - Version {versionStatus.revisionNumber}: - - */} - -
+ ); })} - {/* handleDeletePod(podPendingDelete)} - onNo={() => setPodPendingDelete(null)} - /> */}
)}
@@ -147,87 +82,20 @@ const StyledResourceTab = styled.div` margin-bottom: 2px; font-size: 13px; background: ${(props) => props.theme.fg}; - border-bottom-left-radius: ${(props: { - isLast: boolean; - roundAllCorners: boolean; - }) => (props.isLast ? "10px" : "")}; -`; - -// const Tooltip = styled.div` -// position: absolute; -// right: 0px; -// top: 25px; -// white-space: nowrap; -// height: 18px; -// padding: 2px 5px; -// background: #383842dd; -// display: flex; -// align-items: center; -// justify-content: center; -// flex: 1; -// color: white; -// text-transform: none; -// font-size: 12px; -// outline: 1px solid #ffffff55; -// opacity: 0; -// animation: faded-in 0.2s 0.15s; -// animation-fill-mode: forwards; -// @keyframes faded-in { -// from { -// opacity: 0; -// } -// to { -// opacity: 1; -// } -// } -// `; - -const ExpandWrapper = styled.div``; - -const ResourceHeader = styled.div` - width: 100%; - height: 50px; - display: flex; - font-size: 13px; - align-items: center; - justify-content: space-between; - user-select: none; - padding: 8px 18px; - padding-left: ${(props: { expanded: boolean; hasChildren: boolean }) => - props.hasChildren ? "10px" : "22px"}; - cursor: pointer; - background: ${(props: { expanded: boolean; hasChildren: boolean }) => - props.expanded ? "#ffffff11" : ""}; - :hover { - background: #ffffff18; - - > i { - background: #ffffff22; - } - } + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; `; -const Info = styled.div` +const ExpandWrapper = styled.div` display: flex; - flex-direction: row; - align-items: center; - width: 80%; + flex-direction: column; height: 100%; `; -const Metadata = styled.div` - display: flex; - align-items: center; - position: relative; - max-width: ${(props: { hasStatus: boolean }) => - props.hasStatus ? "calc(100% - 20px)" : "100%"}; -`; - -const Status = styled.div` +export const Status = styled.div` display: flex; width; 20%; font-size: 12px; - text-transform: capitalize; justify-content: flex-end; align-items: center; color: #aaaabb; @@ -238,96 +106,15 @@ const Status = styled.div` } `; -const StatusColor = styled.div` - margin-left: 12px; +export const StatusColor = styled.div<{ color: string }>` + margin-left: 7px; width: 8px; min-width: 8px; height: 8px; - background: ${(props: { status: string }) => - props.status === "running" || - props.status === "Ready" || - props.status === "Completed" - ? "#4797ff" - : props.status === "failed" || props.status === "FailedValidation" - ? "#ed5f85" - : props.status === "completed" - ? "#00d12a" - : "#f5cb42"}; + background: ${({ color }) => color}; border-radius: 20px; `; -const ResourceName = styled.div` - color: #ffffff; - max-width: 40%; - margin-left: ${(props: { showKindLabels: boolean }) => - props.showKindLabels ? "10px" : ""}; - text-transform: none; - white-space: nowrap; -`; - -const IconWrapper = styled.div` - width: 25px; - height: 25px; - display: flex; - align-items: center; - justify-content: center; - - > i { - font-size: 15px; - color: #ffffff; - margin-right: 14px; - } -`; - -const DropdownIcon = styled.div` - > i { - margin-top: 2px; - margin-right: 11px; - font-size: 20px; - color: #ffffff66; - cursor: pointer; - border-radius: 20px; - background: ${(props: { expanded: boolean }) => - props.expanded ? "#ffffff18" : ""}; - transform: ${(props: { expanded: boolean }) => - props.expanded ? "rotate(180deg)" : ""}; - animation: ${(props: { expanded: boolean }) => - props.expanded ? "quarterTurn 0.3s" : ""}; - animation-fill-mode: forwards; - - @keyframes quarterTurn { - from { - transform: rotate(0deg); - } - to { - transform: rotate(90deg); - } - } - } -`; - -const ReplicaSetContainer = styled.div` - padding: 10px 5px; - display: flex; - overflow-wrap: anywhere; - justify-content: space-between; - border-top: 2px solid #ffffff11; -`; - -const ReplicaSetName = styled.span` - padding-left: 10px; - overflow-wrap: anywhere; - max-width: calc(100% - 45px); - line-height: 1.5em; - color: #ffffff33; -`; - -const Bold = styled.span` - font-weight: 500; - display: inline; - color: #ffffff; -`; - const ActionButton = styled.button` position: relative; border: none; @@ -360,16 +147,20 @@ const ServiceHeader = styled.div<{ padding: 20px; color: ${(props) => props.theme.text.primary}; position: relative; - border-radius: 5px; background: ${(props) => props.theme.clickable.bg}; border: 1px solid #494b4f; + border-bottom: ${(props) => + props.showExpanded ? "none" : "1px solid #494b4f"}; :hover { border: 1px solid #7a7b80; + border-bottom: ${(props) => + props.showExpanded ? "none" : "1px solid #7a7b80"}; ${ActionButton} { color: white; } } + border-radius: 5px; border-bottom-left-radius: ${(props) => (props.bordersRounded ? "" : "0")}; border-bottom-right-radius: ${(props) => (props.bordersRounded ? "" : "0")}; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx index 24db12be11..49b5188c0f 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx @@ -1,45 +1,67 @@ import React from "react"; +import dayjs from "dayjs"; import styled from "styled-components"; import Tooltip from "components/porter/Tooltip"; -import { type ClientServiceStatus } from "lib/hooks/useAppStatus"; +import { + statusColor, + type ClientServiceVersionInstanceStatus, +} from "lib/hooks/useAppStatus"; type Props = { - serviceVersionStatus: ClientServiceStatus; + serviceVersionInstanceStatus: ClientServiceVersionInstanceStatus; + isLast: boolean; }; const ServiceVersionInstanceStatus: React.FC = ({ - serviceVersionStatus, + serviceVersionInstanceStatus, + isLast, }) => { return ( - ({})}> - - - - - - - Version: {serviceVersionStatus.revisionNumber} - Restart count: {serviceVersionStatus.restartCount} - Created on: 3 days - {serviceVersionStatus.status === "failing" ? ( - - - Failure Reason: {serviceVersionStatus.crashLoopReason} - - - ) : null} - - } - > - Version: {serviceVersionStatus.revisionNumber} - - - - {serviceVersionStatus.status} - - + + {serviceVersionInstanceStatus.name} + + Restart count: {serviceVersionInstanceStatus.restartCount} + + {`Created: ${dayjs( + serviceVersionInstanceStatus.creationTimestamp + ).format("MMM D, YYYY HH:mm:ss")}`} + + } + containerWidth="100%" + > + ({})}> + + + + + + + + + +
+ + {serviceVersionInstanceStatus.name} + + + + +
+
+
+
); }; @@ -50,11 +72,11 @@ const Grey = styled.div` color: #aaaabb; `; -const FailedStatusContainer = styled.div` +const GutterContainer = styled.div``; +const TooltipContainer = styled.div` + display: flex; width: 100%; - border: 1px solid hsl(0deg, 100%, 30%); - padding: 5px; - margin-block: 5px; + height: 100%; `; const InstanceTooltip = styled.div` @@ -89,26 +111,19 @@ const InstanceTooltip = styled.div` } `; -const Tab = styled.div` +const Tab = styled.div<{ selected: boolean; isLast: boolean }>` width: 100%; - height: 50px; position: relative; display: flex; align-items: center; - justify-content: space-between; - color: ${(props: { selected: boolean }) => - props.selected ? "white" : "#ffffff66"}; background: ${(props: { selected: boolean }) => props.selected ? "#ffffff18" : ""}; font-size: 13px; padding: 20px 19px 20px 42px; text-shadow: 0px 0px 8px none; overflow: visible; - cursor: pointer; - :hover { - color: white; - background: #ffffff18; - } + border: 1px solid #494b4f; + border-bottom: ${(props) => (props.isLast ? "1px solid #494b4f" : "none")}; `; const Rail = styled.div` @@ -143,7 +158,6 @@ const InstanceStatus = styled.div` font-size: 12px; text-transform: capitalize; margin-left: 5px; - justify-content: flex-end; align-items: center; font-family: "Work Sans", sans-serif; color: #aaaabb; @@ -168,20 +182,15 @@ const Name = styled.div` -webkit-line-clamp: 2; `; -const StatusColor = styled.div` +const StatusColor = styled.div<{ color: string }>` margin-left: 12px; width: 8px; min-width: 8px; height: 8px; - background: ${(props: { status: string }) => - props.status === "running" || - props.status === "Ready" || - props.status === "Completed" - ? "#4797ff" - : props.status === "failed" || props.status === "FailedValidation" - ? "#ed5f85" - : props.status === "completed" - ? "#00d12a" - : "#f5cb42"}; + background: ${({ color }) => color}; border-radius: 20px; `; + +const Code = styled.span` + font-family: monospace; +`; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionStatus.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionStatus.tsx index 08f4c2bfb9..dd430f96dd 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionStatus.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionStatus.tsx @@ -1,7 +1,182 @@ -import React from "react"; +import React, { useMemo, useState } from "react"; +import styled from "styled-components"; -const ServiceVersionStatus: React.FC = () => { - return
{/* Your component content goes here */}
; +import Link from "components/porter/Link"; +import Tag from "components/porter/Tag"; +import { type ClientServiceVersionStatus } from "lib/hooks/useAppStatus"; +import { + isClientRevisionNotification, + isClientServiceNotification, +} from "lib/porter-apps/notification"; + +import alert from "assets/alert-warning.svg"; + +import { useLatestRevision } from "../../LatestRevisionContext"; +import ServiceVersionInstanceStatus from "./ServiceVersionInstanceStatus"; +import StatusTags from "./StatusTags"; + +type Props = { + serviceVersionStatus: ClientServiceVersionStatus; + serviceName: string; +}; +const ServiceVersionStatus: React.FC = ({ + serviceVersionStatus, + serviceName, +}) => { + const [expanded, setExpanded] = useState(false); + + const { tabUrlGenerator, latestClientNotifications } = useLatestRevision(); + + const notificationsExistForServiceVersion = useMemo(() => { + return ( + latestClientNotifications + .filter(isClientRevisionNotification) + .some((n) => n.appRevisionId === serviceVersionStatus.revisionId) || + latestClientNotifications + .filter(isClientServiceNotification) + .some( + (n) => + n.appRevisionId === serviceVersionStatus.revisionId && + n.service.name.value === serviceName + ) + ); + }, [ + JSON.stringify(latestClientNotifications), + JSON.stringify(serviceVersionStatus), + serviceName, + ]); + + return ( + + { + setExpanded(!expanded); + }} + > + + + arrow_right + + + Version {serviceVersionStatus.revisionNumber} + + +
+ + + {notificationsExistForServiceVersion && ( + + + + Notifications + + + )} +
+
+ {expanded && ( + + {serviceVersionStatus.instanceStatusList.map((instanceStatus, i) => ( + + ))} + + )} +
+ ); }; export default ServiceVersionStatus; + +const StyledServiceVersionStatus = styled.div``; + +const ServiceVersionInstanceStatusContainer = styled.div` + display: flex; + flex-direction: column; + height: 100%; +`; + +const ResourceHeader = styled.div<{ expanded: boolean }>` + width: 100%; + height: 50px; + display: flex; + font-size: 13px; + align-items: center; + justify-content: space-between; + user-select: none; + padding: 8px 18px; + cursor: pointer; + border: 1px solid #494b4f; + border-bottom: ${(props) => (props.expanded ? "none" : "1px solid #494b4f")}; + + :hover { + background: #ffffff18; + + > i { + background: #ffffff22; + } + } +`; + +const Info = styled.div` + display: flex; + flex-direction: row; + align-items: center; + width: 80%; + height: 100%; +`; + +const DropdownIcon = styled.div` + > i { + margin-top: 2px; + margin-right: 11px; + font-size: 20px; + color: #ffffff66; + cursor: pointer; + border-radius: 20px; + background: ${(props: { expanded: boolean }) => + props.expanded ? "#ffffff18" : ""}; + transform: ${(props: { expanded: boolean }) => + props.expanded ? "rotate(180deg)" : ""}; + animation: ${(props: { expanded: boolean }) => + props.expanded ? "quarterTurn 0.3s" : ""}; + animation-fill-mode: forwards; + + @keyframes quarterTurn { + from { + transform: rotate(0deg); + } + to { + transform: rotate(90deg); + } + } + } +`; + +const ReplicaSetName = styled.span` + padding-left: 10px; + overflow-wrap: anywhere; + max-width: calc(100% - 45px); + line-height: 1.5em; + color: #ffffff33; +`; + +const Bold = styled.span` + font-weight: 500; + display: inline; + color: #ffffff; +`; + +const TagIcon = styled.img` + height: 12px; + margin-right: 3px; +`; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/StatusTags.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/StatusTags.tsx new file mode 100644 index 0000000000..45e77ba3fc --- /dev/null +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/StatusTags.tsx @@ -0,0 +1,158 @@ +import React, { useMemo } from "react"; +import pluralize from "pluralize"; +import styled from "styled-components"; + +import Spacer from "components/porter/Spacer"; +import Tag from "components/porter/Tag"; +import Text from "components/porter/Text"; +import Tooltip from "components/porter/Tooltip"; +import { + statusColor, + type ServiceStatusDescriptor, +} from "lib/hooks/useAppStatus"; + +import { Status, StatusColor } from "./ServiceStatus"; + +type StatusType = { + status: ServiceStatusDescriptor; +}; + +type Props = { + statusList: T; +}; +const StatusTags: React.FC> = ({ statusList }) => { + const statusSummary = useMemo(() => { + return [ + statusList.filter((i) => i.status === "running").length, + statusList.filter((i) => i.status === "pending").length, + statusList.filter((i) => i.status === "failing").length, + ]; + }, [JSON.stringify(statusList)]); + + return ( + + + {`${statusSummary[0]} ${pluralize( + "instance", + statusSummary[0] + )} ${pluralize("is", statusSummary[0])} currently running`} + + } + backgroundColor="" + > + + + + {statusSummary[0]} + + + + + + + + {`${statusSummary[1]} ${pluralize( + "instance", + statusSummary[1] + )} ${pluralize("is", statusSummary[1])} currently pending`} + + } + backgroundColor="" + > + + + + {statusSummary[1]} + + + + + + + + {`${statusSummary[2]} ${pluralize( + "instance", + statusSummary[2] + )} ${pluralize("is", statusSummary[2])} currently failing`} + + } + backgroundColor="" + > + + + + {statusSummary[2]} + + + + + + + ); +}; + +export default StatusTags; + +const InnerTag = styled.div` + display: flex; + align-items: center; + width: 25px; +`; + +const TooltipContent = styled.div` + position: absolute; + left: 35px; + word-wrap: break-word; + top: 38px; + min-height: 18px; + max-width: calc(100% - 75px); + padding: 5px 7px; + background: #272731; + z-index: 999; + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; + color: white; + text-transform: none; + font-size: 12px; + font-family: "Work Sans", sans-serif; + outline: 1px solid #ffffff55; + opacity: 0; + animation: faded-in 0.2s 0.15s; + animation-fill-mode: forwards; + @keyframes faded-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } +`; + +const Grey = styled.div` + margin-top: 5px; + color: #aaaabb; +`; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx index 28f476bced..83ccc09791 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx @@ -6,7 +6,7 @@ import styled, { keyframes } from "styled-components"; import { match } from "ts-pattern"; import Spacer from "components/porter/Spacer"; -import { type ClientServiceStatus } from "lib/hooks/useAppStatus"; +import { type ClientServiceVersionInstanceStatus } from "lib/hooks/useAppStatus"; import { type PorterAppFormData } from "lib/porter-apps"; import { type ClientService } from "lib/porter-apps/services"; @@ -28,7 +28,7 @@ type ServiceProps = { "app.services" | "app.predeploy" >; remove: (index: number) => void; - status?: ClientServiceStatus[]; + status?: ClientServiceVersionInstanceStatus[]; maxCPU: number; maxRAM: number; clusterContainsGPUNodes: boolean; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx index 71dab62f1d..ddf892326e 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx @@ -16,7 +16,7 @@ import Modal from "components/porter/Modal"; import Select from "components/porter/Select"; import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; -import { type ClientServiceStatus } from "lib/hooks/useAppStatus"; +import { type ClientServiceVersionInstanceStatus } from "lib/hooks/useAppStatus"; import { type PorterAppFormData } from "lib/porter-apps"; import { defaultSerialized, @@ -50,7 +50,7 @@ type ServiceListProps = { isPredeploy?: boolean; existingServiceNames?: string[]; fieldArrayName: "app.services" | "app.predeploy"; - serviceVersionStatus?: Record; + serviceVersionStatus?: Record; internalNetworkingDetails?: { namespace: string; appName: string; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceStatusFooter.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceStatusFooter.tsx index 6874795637..fe6bfd59bd 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceStatusFooter.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceStatusFooter.tsx @@ -10,7 +10,7 @@ import Link from "components/porter/Link"; import Spacer from "components/porter/Spacer"; import Tag from "components/porter/Tag"; import Text from "components/porter/Text"; -import { type ClientServiceStatus } from "lib/hooks/useAppStatus"; +import { type ClientServiceVersionInstanceStatus } from "lib/hooks/useAppStatus"; import { isClientServiceNotification } from "lib/porter-apps/notification"; import alert from "assets/alert-warning.svg"; @@ -20,7 +20,7 @@ import TriggerJobButton from "../jobs/TriggerJobButton"; type ServiceStatusFooterProps = { serviceName: string; - status: ClientServiceStatus[]; + status: ClientServiceVersionInstanceStatus[]; isJob: boolean; }; const ServiceStatusFooter: React.FC = ({ @@ -110,7 +110,7 @@ const ServiceStatusFooter: React.FC = ({ ); }) - .with({ status: "spinningDown" }, (vs) => { + .with({ status: "pending" }, (vs) => { return ( diff --git a/internal/porter_app/status.go b/internal/porter_app/status.go index 46afc7d9d7..4908025bfb 100644 --- a/internal/porter_app/status.go +++ b/internal/porter_app/status.go @@ -56,6 +56,7 @@ type InstanceStatus struct { Status InstanceStatusDescriptor `json:"status"` RestartCount int `json:"restart_count"` CreationTimestamp time.Time `json:"creation_timestamp"` + Name string `json:"name"` } // GetServiceStatusInput is the input type for GetServiceStatus @@ -199,7 +200,9 @@ func instanceStatusFromPod(ctx context.Context, inp instanceStatusFromPodInput) telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "pod-name", Value: inp.Pod.Name}) - instanceStatus := InstanceStatus{} + instanceStatus := InstanceStatus{ + Name: inp.Pod.Name, + } // find the container running the app code. Note that this is conditioned on the fact that // in our worker/web/job charts, there is one container created with this name during the deployment From 6c8c8f87c0ad055e3dfa02c6d123cba2f88983b2 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Wed, 3 Jan 2024 16:26:38 -0500 Subject: [PATCH 4/7] style nits --- .../src/main/home/app-dashboard/app-view/tabs/StatusTab.tsx | 1 + .../app-view/tabs/status/ServiceVersionInstanceStatus.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/StatusTab.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/StatusTab.tsx index d8f78fbc95..137840795a 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/StatusTab.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/StatusTab.tsx @@ -40,6 +40,7 @@ const StatusTab: React.FC = () => { return ( {Object.keys(appServiceStatus) + .sort() .map((serviceName) => { const serviceStatus = appServiceStatus[serviceName]; const clientService = latestClientServices.find( diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx index 49b5188c0f..904787160e 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx @@ -31,6 +31,7 @@ const ServiceVersionInstanceStatus: React.FC = ({ } containerWidth="100%" + tooltipContentWidth="300px" > ({})}> From 526589a09e91d3afcadcadb10ceab0a3741b793d Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Mon, 1 Apr 2024 13:12:31 -0400 Subject: [PATCH 5/7] fix status footer --- .../handlers/porter_app/service_status.go | 7 +- dashboard/src/lib/hooks/useAppStatus.ts | 16 +- .../app-dashboard/app-view/tabs/Overview.tsx | 4 +- .../app-view/tabs/status/ServiceStatus.tsx | 2 + .../services-settings/ServiceContainer.tsx | 7 +- .../services-settings/ServiceList.tsx | 8 +- .../footers/ServiceStatusFooter.tsx | 160 +++++++----------- 7 files changed, 87 insertions(+), 117 deletions(-) diff --git a/api/server/handlers/porter_app/service_status.go b/api/server/handlers/porter_app/service_status.go index a71dae60f7..4b54835d30 100644 --- a/api/server/handlers/porter_app/service_status.go +++ b/api/server/handlers/porter_app/service_status.go @@ -138,8 +138,13 @@ func (c *ServiceStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) return } + appRevisions := listAppRevisionsResp.Msg.AppRevisions + if appRevisions == nil { + appRevisions = []*porterv1.AppRevision{} + } + var revisions []porter_app.Revision - for _, revision := range listAppRevisionsResp.Msg.AppRevisions { + for _, revision := range appRevisions { encodedRevision, err := porter_app.EncodedRevisionFromProto(ctx, revision) if err != nil { err := telemetry.Error(ctx, span, err, "error getting encoded revision from proto") diff --git a/dashboard/src/lib/hooks/useAppStatus.ts b/dashboard/src/lib/hooks/useAppStatus.ts index 23ecf5a2e0..504ff66a0c 100644 --- a/dashboard/src/lib/hooks/useAppStatus.ts +++ b/dashboard/src/lib/hooks/useAppStatus.ts @@ -11,6 +11,8 @@ import { type NewWebsocketOptions, } from "shared/hooks/useWebsockets"; +export type AppServiceStatus = Record; + export type ServiceStatusDescriptor = | "running" | "pending" @@ -32,7 +34,7 @@ export type ClientServiceVersionStatus = { export type ClientServiceVersionInstanceStatus = { status: ServiceStatusDescriptor; - message: string; + revisionId: string; crashLoopReason: string; restartCount: number; name: string; @@ -73,7 +75,7 @@ export const useAppStatus = ({ appName: string; kind?: string; }): { - appServiceStatus: Record; + appServiceStatus: AppServiceStatus; } => { const [serviceStatusMap, setServiceStatusMap] = useState< Record @@ -179,22 +181,14 @@ export const useAppStatus = ({ .otherwise(() => "unknown" as const); const clientServiceVersionInstanceStatus: ClientServiceVersionInstanceStatus = { + revisionId: revisionStatus.revision_id, status, - message: "", crashLoopReason: "", restartCount: instanceStatus.restart_count, name: instanceStatus.name, creationTimestamp: instanceStatus.creation_timestamp, }; - if (instanceStatus.status === "PENDING") { - clientServiceVersionInstanceStatus.message = `Instance is pending at Version ${revisionStatus.revision_number}`; - } else if (instanceStatus.status === "RUNNING") { - clientServiceVersionInstanceStatus.message = `Instance is running at Version ${revisionStatus.revision_number}`; - } else if (instanceStatus.status === "FAILED") { - clientServiceVersionInstanceStatus.message = `Instance is failing at Version ${revisionStatus.revision_number}`; - } - return clientServiceVersionInstanceStatus; }); diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx index 05490ea65a..28d6e12f49 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx @@ -36,7 +36,7 @@ const Overview: React.FC = ({ buttonStatus }) => { clusterId, }); - const { serviceVersionStatus } = useAppStatus({ + const { appServiceStatus } = useAppStatus({ projectId, clusterId, services: latestClientServices.filter( @@ -63,7 +63,7 @@ const Overview: React.FC = ({ buttonStatus }) => { addNewText={"Add a new service"} fieldArrayName={"app.services"} existingServiceNames={latestProto.serviceList.map((s) => s.name)} - serviceVersionStatus={serviceVersionStatus} + appServiceStatus={appServiceStatus} internalNetworkingDetails={{ namespace: deploymentTarget.namespace, appName: porterApp.name, diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceStatus.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceStatus.tsx index e626cc1480..fa891ae868 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceStatus.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceStatus.tsx @@ -31,6 +31,8 @@ const ServiceStatus: React.FC = ({ serviceStatus, service }) => { return ; case "predeploy": return ; + case "initdeploy": + return ; } }; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx index 3c953e60f3..4f140bd880 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx @@ -6,7 +6,7 @@ import styled, { keyframes } from "styled-components"; import { match } from "ts-pattern"; import Spacer from "components/porter/Spacer"; -import { type ClientServiceVersionInstanceStatus } from "lib/hooks/useAppStatus"; +import { type ClientServiceStatus } from "lib/hooks/useAppStatus"; import { type PorterAppFormData } from "lib/porter-apps"; import { isClientJobService, @@ -33,7 +33,7 @@ type ServiceProps = { "app.services" | "app.predeploy" >; remove: (index: number) => void; - status?: ClientServiceStatus[]; + status?: ClientServiceStatus; internalNetworkingDetails: { namespace: string; appName: string; @@ -68,6 +68,7 @@ const ServiceContainer: React.FC = ({ .with({ config: { type: "predeploy" } }, (svc) => ( )) + .with({ config: { type: "initdeploy" } }, () => <>) .exhaustive(); }; @@ -81,6 +82,8 @@ const ServiceContainer: React.FC = ({ return ; case "predeploy": return ; + case "initdeploy": + return ; } }; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx index fa82786197..b1b6246a07 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx @@ -16,7 +16,7 @@ import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider"; import { type ClientCluster } from "lib/clusters/types"; -import { type ClientServiceStatus } from "lib/hooks/useAppStatus"; +import { type AppServiceStatus } from "lib/hooks/useAppStatus"; import { type PorterAppFormData } from "lib/porter-apps"; import { defaultSerialized, @@ -50,7 +50,7 @@ type ServiceListProps = { isPredeploy?: boolean; existingServiceNames?: string[]; fieldArrayName: "app.services" | "app.predeploy"; - serviceVersionStatus?: Record; + appServiceStatus?: AppServiceStatus; internalNetworkingDetails?: { namespace: string; appName: string; @@ -64,7 +64,7 @@ const ServiceList: React.FC = ({ fieldArrayName, isPredeploy = false, existingServiceNames = [], - serviceVersionStatus, + appServiceStatus, internalNetworkingDetails = { namespace: "", appName: "", @@ -238,7 +238,7 @@ const ServiceList: React.FC = ({ service={svc} update={update} remove={onRemove} - status={serviceVersionStatus?.[svc.name.value]} + status={appServiceStatus?.[svc.name.value]} internalNetworkingDetails={internalNetworkingDetails} existingServiceNames={existingServiceNames} /> diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/footers/ServiceStatusFooter.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/footers/ServiceStatusFooter.tsx index 3357d673b8..9718b72083 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/footers/ServiceStatusFooter.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/footers/ServiceStatusFooter.tsx @@ -1,15 +1,17 @@ -import React, { useState } from "react"; +import React, { useMemo } from "react"; import _ from "lodash"; -import AnimateHeight, { type Height } from "react-animate-height"; +import pluralize from "pluralize"; import styled from "styled-components"; import { match } from "ts-pattern"; -import Button from "components/porter/Button"; import Container from "components/porter/Container"; import Link from "components/porter/Link"; import Tag from "components/porter/Tag"; import Text from "components/porter/Text"; -import { type ClientServiceVersionInstanceStatus } from "lib/hooks/useAppStatus"; +import { + type ClientServiceStatus, + type ServiceStatusDescriptor, +} from "lib/hooks/useAppStatus"; import { isClientServiceNotification } from "lib/porter-apps/notification"; import alert from "assets/alert-warning.svg"; @@ -17,78 +19,78 @@ import alert from "assets/alert-warning.svg"; import { useLatestRevision } from "../../../app-view/LatestRevisionContext"; type ServiceStatusFooterProps = { - status: ClientServiceVersionInstanceStatus[]; + status: ClientServiceStatus; name: string; }; const ServiceStatusFooter: React.FC = ({ status, name, }) => { - const [expanded, setExpanded] = useState(false); const { latestClientNotifications, tabUrlGenerator } = useLatestRevision(); - const [height, setHeight] = useState(0); + + // group instances by revision number and status + const instanceGroups: Array<{ + revisionId: string; + revisionNumber: number; + status: ServiceStatusDescriptor; + numInstances: number; + restartCount: number; + }> = useMemo(() => { + return status.versionStatusList + .map((versionStatus) => { + const groupByStatus = _.groupBy( + versionStatus.instanceStatusList, + (instance) => instance.status + ); + return Object.keys(groupByStatus).map((status) => { + return { + revisionId: versionStatus.revisionId, + revisionNumber: versionStatus.revisionNumber, + status: status as ServiceStatusDescriptor, + numInstances: groupByStatus[status].length, + restartCount: groupByStatus[status].reduce( + (acc, instance) => acc + instance.restartCount, + 0 + ), + }; + }); + }) + .flat() + .filter((group) => group.status !== "unknown"); + }, [status]); return ( - <> - {status.map((versionStatus, i) => { +
+ {instanceGroups.map((instanceGroup, i) => { const versionNotifications = latestClientNotifications .filter(isClientServiceNotification) - .filter((n) => n.appRevisionId === versionStatus.revisionId) + .filter((n) => n.appRevisionId === instanceGroup.revisionId) .filter((n) => n.service.name.value === name); return (
- + - {match(versionStatus) - .with({ status: "failing" }, (vs) => { - return ( - <> - - - {vs.message} - - {vs.crashLoopReason && ( - - )} - - ); - }) - .with({ status: "pending" }, (vs) => { - return ( - - - {vs.message} - - ); - }) - .with({ status: "running" }, (vs) => { - return ( - - - {vs.message} - - ); - }) - .exhaustive()} + + + {`${ + instanceGroup.numInstances + } ${pluralize( + "instance", + instanceGroup.numInstances + )} ${pluralize("is", instanceGroup.numInstances)} ${match( + instanceGroup + ) + .with({ status: "failing" }, () => "failing to run") + .with({ status: "pending" }, () => "pending") + .with({ status: "running" }, () => "running") + .otherwise(() => "")} at Version ${ + instanceGroup.revisionNumber + }`} + - {(versionStatus.restartCount ?? 0) > 0 && ( + {(instanceGroup.restartCount ?? 0) > 0 && ( - Restarts: {versionStatus.restartCount} + Restarts: {instanceGroup.restartCount} )} {versionNotifications.length > 0 && ( @@ -107,17 +109,10 @@ const ServiceStatusFooter: React.FC = ({ - {versionStatus.crashLoopReason && ( - - - {versionStatus.crashLoopReason} - - - )}
); })} - +
); }; @@ -152,11 +147,6 @@ const StatusDot = styled.div<{ color?: string }>` } `; -const I = styled.i` - font-size: 14px; - margin-right: 5px; -`; - const Running = styled.div` display: flex; align-items: center; @@ -185,32 +175,8 @@ const StyledStatusFooter = styled.div` } `; -const StyledStatusFooterTop = styled(StyledStatusFooter)<{ - expanded: boolean; -}>` +const StyledStatusFooterTop = styled(StyledStatusFooter)` height: 40px; - border-bottom: ${({ expanded }) => expanded && "0px"}; - border-bottom-left-radius: ${({ expanded }) => expanded && "0px"}; - border-bottom-right-radius: ${({ expanded }) => expanded && "0px"}; -`; - -const Message = styled.div` - padding: 20px; - background: #000000; - border-radius: 5px; - line-height: 1.5em; - border: 1px solid #aaaabb33; - font-family: monospace; - font-size: 13px; - display: flex; - align-items: center; - > img { - width: 13px; - margin-right: 20px; - } - width: 100%; - height: 101px; - overflow: hidden; `; const StyledContainer = styled.div<{ From 304acbbbb8a9177a2b6ec3c3dc24227959de9ea7 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Mon, 1 Apr 2024 13:24:06 -0400 Subject: [PATCH 6/7] fix tooltips --- .../status/ServiceVersionInstanceStatus.tsx | 38 +---------- .../app-view/tabs/status/StatusTags.tsx | 65 ++++--------------- 2 files changed, 15 insertions(+), 88 deletions(-) diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx index 904787160e..533be729de 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/ServiceVersionInstanceStatus.tsx @@ -20,15 +20,15 @@ const ServiceVersionInstanceStatus: React.FC = ({ +
{serviceVersionInstanceStatus.name} Restart count: {serviceVersionInstanceStatus.restartCount} {`Created: ${dayjs( serviceVersionInstanceStatus.creationTimestamp - ).format("MMM D, YYYY HH:mm:ss")}`} - + ).format("MMM D, YYYY HH:mm:ss Z")}`} +
} containerWidth="100%" tooltipContentWidth="300px" @@ -80,38 +80,6 @@ const TooltipContainer = styled.div` height: 100%; `; -const InstanceTooltip = styled.div` - position: absolute; - left: 35px; - word-wrap: break-word; - top: 38px; - min-height: 18px; - max-width: calc(100% - 75px); - padding: 5px 7px; - background: #272731; - z-index: 999; - display: flex; - flex-direction: column; - justify-content: center; - flex: 1; - color: white; - text-transform: none; - font-size: 12px; - font-family: "Work Sans", sans-serif; - outline: 1px solid #ffffff55; - opacity: 0; - animation: faded-in 0.2s 0.15s; - animation-fill-mode: forwards; - @keyframes faded-in { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - const Tab = styled.div<{ selected: boolean; isLast: boolean }>` width: 100%; position: relative; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/StatusTags.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/StatusTags.tsx index 45e77ba3fc..2db3323fef 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/status/StatusTags.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/status/StatusTags.tsx @@ -33,12 +33,12 @@ const StatusTags: React.FC> = ({ statusList }) => { - {`${statusSummary[0]} ${pluralize( +
+ {`${statusSummary[0]} ${pluralize( "instance", statusSummary[0] - )} ${pluralize("is", statusSummary[0])} currently running`} - + )} ${pluralize("is", statusSummary[0])} currently running`} +
} backgroundColor="" > @@ -59,12 +59,10 @@ const StatusTags: React.FC> = ({ statusList }) => { - {`${statusSummary[1]} ${pluralize( - "instance", - statusSummary[1] - )} ${pluralize("is", statusSummary[1])} currently pending`} - +
{`${statusSummary[1]} ${pluralize( + "instance", + statusSummary[1] + )} ${pluralize("is", statusSummary[1])} currently pending`}
} backgroundColor="" > @@ -85,12 +83,10 @@ const StatusTags: React.FC> = ({ statusList }) => { - {`${statusSummary[2]} ${pluralize( - "instance", - statusSummary[2] - )} ${pluralize("is", statusSummary[2])} currently failing`} - +
{`${statusSummary[2]} ${pluralize( + "instance", + statusSummary[2] + )} ${pluralize("is", statusSummary[2])} currently failing`}
} backgroundColor="" > @@ -119,40 +115,3 @@ const InnerTag = styled.div` align-items: center; width: 25px; `; - -const TooltipContent = styled.div` - position: absolute; - left: 35px; - word-wrap: break-word; - top: 38px; - min-height: 18px; - max-width: calc(100% - 75px); - padding: 5px 7px; - background: #272731; - z-index: 999; - display: flex; - flex-direction: column; - justify-content: center; - flex: 1; - color: white; - text-transform: none; - font-size: 12px; - font-family: "Work Sans", sans-serif; - outline: 1px solid #ffffff55; - opacity: 0; - animation: faded-in 0.2s 0.15s; - animation-fill-mode: forwards; - @keyframes faded-in { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - -const Grey = styled.div` - margin-top: 5px; - color: #aaaabb; -`; From 59ef5238247075ffcae3f81018e873ff394d3db6 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Mon, 1 Apr 2024 13:38:28 -0400 Subject: [PATCH 7/7] condition on beta features enabled --- .../app-view/AppDataContainer.tsx | 102 ++++++++++++------ 1 file changed, 68 insertions(+), 34 deletions(-) diff --git a/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx b/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx index 9d77990b8b..5ac1b3634a 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx @@ -455,40 +455,74 @@ const AppDataContainer: React.FC = ({ tabParam }) => { const tabs = useMemo(() => { const numNotifications = latestClientNotifications.length; - const base = [ - { - label: `Notifications`, - value: "notifications", - sibling: - numNotifications > 0 ? ( - - - -
- {numNotifications} -
- -
- ) : undefined, - }, - { label: "Activity", value: "activity" }, - { label: "Status", value: "status" }, - { label: "Logs", value: "logs" }, - { label: "Metrics", value: "metrics" }, - { label: "Services", value: "overview" }, - { label: "Environment", value: "environment" }, - ]; + const base = currentProject?.beta_features_enabled + ? [ + { + label: `Notifications`, + value: "notifications", + sibling: + numNotifications > 0 ? ( + + + +
+ {numNotifications} +
+ +
+ ) : undefined, + }, + { label: "Activity", value: "activity" }, + { label: "Status", value: "status" }, + { label: "Logs", value: "logs" }, + { label: "Metrics", value: "metrics" }, + { label: "Services", value: "overview" }, + { label: "Environment", value: "environment" }, + ] + : [ + { + label: `Notifications`, + value: "notifications", + sibling: + numNotifications > 0 ? ( + + + +
+ {numNotifications} +
+ +
+ ) : undefined, + }, + { label: "Activity", value: "activity" }, + { label: "Overview", value: "overview" }, + { label: "Logs", value: "logs" }, + { label: "Metrics", value: "metrics" }, + { label: "Environment", value: "environment" }, + ]; if (deploymentTarget.is_preview) { return base;