Feroze Mohideen 2 år sedan
förälder
incheckning
f4e3227679

+ 11 - 7
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;

+ 5 - 1
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<AppDataContainerProps> = ({ 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<AppDataContainerProps> = ({ tabParam }) => {
             />
           ))
           .with("notifications", () => <Notifications />)
+          .with("status", () => <StatusTab />)
           .otherwise(() => null)}
         <Spacer y={2} />
       </form>

+ 188 - 0
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 (
+  //       <Logs
+  //         podError={podError}
+  //         key={selectedPod?.metadata?.name}
+  //         selectedPod={selectedPod}
+  //       />
+  //     );
+  //   };
+
+  //   const renderTabs = () => {
+  //     return controllers.map((c, i) => {
+  //       return (
+  //         <ControllerTab
+  //           // handle CronJob case
+  //           key={c.metadata?.uid || c.uid}
+  //           selectedPod={selectedPod}
+  //           selectPod={setSelectedPod}
+  //           selectors={selectors ? [selectors[i]] : null}
+  //           controller={c}
+  //           isLast={i === controllers?.length - 1}
+  //           isFirst={i === 0}
+  //           setPodError={(x: string) => setPodError(x)}
+  //         />
+  //       );
+  //     });
+  //   };
+
+  const renderStatusSection = (): JSX.Element => {
+    if (Object.keys(serviceVersionStatus).length === 0) {
+      return (
+        <NoControllers>
+          <Loading />
+        </NoControllers>
+      );
+    }
+
+    return (
+      <ServiceVersionContainer>
+        {Object.keys(serviceVersionStatus)
+          .map((serviceName, i) => {
+            const serviceStatus = serviceVersionStatus[serviceName];
+            const clientService = latestClientServices.find(
+              (s) => s.name.value === serviceName
+            );
+            if (clientService) {
+              return (
+                <ServiceStatus
+                  isLast={i === Object.keys(serviceVersionStatus).length - 1}
+                  key={serviceName}
+                  serviceVersionStatusList={serviceStatus}
+                  service={clientService}
+                />
+              );
+            }
+            return null;
+          })
+          .filter(valueExists)}
+      </ServiceVersionContainer>
+    );
+    // if (controllers?.length > 0) {
+    //   return (
+    //     <Wrapper>
+    //       <TabWrapper>{renderTabs()}</TabWrapper>
+    //       {renderLogs()}
+    //     </Wrapper>
+    //   );
+    // }
+
+    // if (currentChart?.chart?.metadata?.name === "job") {
+    //   return (
+    //     <NoControllers>
+    //       <i className="material-icons">category</i>
+    //       There are no jobs currently running.
+    //     </NoControllers>
+    //   );
+    // }
+
+    // return (
+    //   <NoControllers>
+    //     <i className="material-icons">category</i>
+    //     No objects to display. This might happen while your app is still
+    //     deploying.
+    //   </NoControllers>
+    // );
+  };
+
+  return <StyledStatusSection>{renderStatusSection()}</StyledStatusSection>;
+};
+
+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;
+`;

+ 389 - 0
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<Props> = ({
+  isLast,
+  serviceVersionStatusList,
+  service,
+}) => {
+  const [expanded, setExpanded] = useState<boolean>(false);
+
+  const renderIcon = (service: ClientService): JSX.Element => {
+    switch (service.config.type) {
+      case "web":
+        return <Icon src={web} />;
+      case "worker":
+        return <Icon src={worker} />;
+      case "job":
+        return <Icon src={job} />;
+      case "predeploy":
+        return <Icon src={job} />;
+    }
+  };
+
+  return (
+    <StyledResourceTab
+      isLast={isLast}
+      onClick={() => ({})}
+      roundAllCorners={true}
+    >
+      {/* <ResourceHeader
+        hasChildren={true}
+        expanded={expanded}
+        onClick={() => {
+          setExpanded(!expanded);
+        }}
+      >
+        <Info>
+          <DropdownIcon expanded={expanded}>
+            <i className="material-icons">arrow_right</i>
+          </DropdownIcon>
+          <ServiceNameTag>
+            {match(service.config.type)
+              .with("web", () => <ServiceTypeIcon src={web} />)
+              .with("worker", () => <ServiceTypeIcon src={worker} />)
+              .with("job", () => <ServiceTypeIcon src={job} />)
+              .with("predeploy", () => <ServiceTypeIcon src={job} />)
+              .exhaustive()}
+            <Spacer inline x={0.5} />
+            {service.name.value}
+          </ServiceNameTag>
+        </Info>
+        <Status>
+          test status
+          <StatusColor status={"running"} />
+        </Status>
+      </ResourceHeader> */}
+      <ServiceHeader
+        showExpanded={expanded}
+        onClick={() => {
+          setExpanded(!expanded);
+        }}
+        bordersRounded={!expanded}
+      >
+        <ServiceTitle>
+          <ActionButton>
+            <span className="material-icons dropdown">arrow_drop_down</span>
+          </ActionButton>
+          {renderIcon(service)}
+          <Spacer inline x={1} />
+          {service.name.value}
+        </ServiceTitle>
+
+        <Status>
+          test status
+          <StatusColor status={"running"} />
+        </Status>
+      </ServiceHeader>
+      {expanded && (
+        <ExpandWrapper>
+          {serviceVersionStatusList.map((versionStatus) => {
+            return (
+              <div key={versionStatus.revisionId}>
+                <ResourceHeader
+                  hasChildren={true}
+                  expanded={expanded}
+                  onClick={() => {
+                    setExpanded(!expanded);
+                  }}
+                >
+                  <Info>
+                    <DropdownIcon expanded={expanded}>
+                      <i className="material-icons">arrow_right</i>
+                    </DropdownIcon>
+                    <ReplicaSetName>
+                      <Bold>Version {versionStatus.revisionNumber}:</Bold>
+                    </ReplicaSetName>
+                  </Info>
+                  <Status>
+                    test status
+                    <StatusColor status={"running"} />
+                  </Status>
+                </ResourceHeader>
+                {/* <ReplicaSetContainer>
+                  <ReplicaSetName>
+                    <Bold>Version {versionStatus.revisionNumber}:</Bold>
+                  </ReplicaSetName>
+                </ReplicaSetContainer> */}
+                <ServiceVersionInstanceStatus
+                  serviceVersionStatus={versionStatus}
+                />
+              </div>
+            );
+          })}
+          {/* <ConfirmOverlay
+            message="Are you sure you want to delete this pod?"
+            show={podPendingDelete}
+            onYes={() => handleDeletePod(podPendingDelete)}
+            onNo={() => setPodPendingDelete(null)}
+          /> */}
+        </ExpandWrapper>
+      )}
+    </StyledResourceTab>
+  );
+};
+
+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;
+`;

+ 187 - 0
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<Props> = ({
+  serviceVersionStatus,
+}) => {
+  return (
+    <Tab selected={true} onClick={() => ({})}>
+      <Gutter>
+        <Rail />
+        <Circle />
+        <Rail lastTab={false} />
+      </Gutter>
+      <Tooltip
+        content={
+          <InstanceTooltip>
+            Version: {serviceVersionStatus.revisionNumber}
+            <Grey>Restart count: {serviceVersionStatus.restartCount}</Grey>
+            <Grey>Created on: 3 days</Grey>
+            {serviceVersionStatus.status === "failing" ? (
+              <FailedStatusContainer>
+                <Grey>
+                  Failure Reason: {serviceVersionStatus.crashLoopReason}
+                </Grey>
+              </FailedStatusContainer>
+            ) : null}
+          </InstanceTooltip>
+        }
+      >
+        <Name>Version: {serviceVersionStatus.revisionNumber}</Name>
+      </Tooltip>
+      <InstanceStatus>
+        <StatusColor status={"running"} />
+        {serviceVersionStatus.status}
+      </InstanceStatus>
+    </Tab>
+  );
+};
+
+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;
+`;

+ 7 - 0
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 <div>{/* Your component content goes here */}</div>;
+};
+
+export default ServiceVersionStatus;

+ 3 - 3
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")};