Bläddra i källkod

Implement functionality for preview revisions

jnfrati 3 år sedan
förälder
incheckning
6e07eb9734

+ 79 - 26
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack.tsx → dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx

@@ -1,4 +1,6 @@
 import Loading from "components/Loading";
+import Placeholder from "components/Placeholder";
+import TabSelector from "components/TabSelector";
 import TitleSection from "components/TitleSection";
 import React, { useContext, useEffect, useState } from "react";
 import { useParams } from "react-router";
@@ -6,9 +8,9 @@ import api from "shared/api";
 import { Context } from "shared/Context";
 import { readableDate } from "shared/string_utils";
 import styled from "styled-components";
-import ChartList from "../chart/ChartList";
-import SortSelector from "../SortSelector";
-import Status from "./components/Status";
+import ChartList from "../../chart/ChartList";
+import SortSelector from "../../SortSelector";
+import Status from "../components/Status";
 import {
   Br,
   InfoWrapper,
@@ -16,9 +18,10 @@ import {
   LineBreak,
   SepDot,
   Text,
-} from "./components/styles";
-import { getStackStatus, getStackStatusMessage } from "./shared";
-import { Stack } from "./types";
+} from "../components/styles";
+import { getStackStatus, getStackStatusMessage } from "../shared";
+import { Stack, StackRevision } from "../types";
+import RevisionList from "./_RevisionList";
 
 const ExpandedStack = () => {
   const { namespace, stack_id } = useParams<{
@@ -30,13 +33,16 @@ const ExpandedStack = () => {
   const [stack, setStack] = useState<Stack>();
   const [sortType, setSortType] = useState("Alphabetical");
   const [isLoading, setIsLoading] = useState(true);
+  const [currentTab, setCurrentTab] = useState("apps");
+
+  const [currentRevision, setCurrentRevision] = useState<StackRevision>();
 
   useEffect(() => {
     console.log(stack_id);
     let isSubscribed = true;
 
     api
-      .getStack(
+      .getStack<Stack>(
         "<token>",
         {},
         {
@@ -49,6 +55,7 @@ const ExpandedStack = () => {
       .then((res) => {
         if (isSubscribed) {
           setStack(res.data);
+          setCurrentRevision(res.data.latest_revision);
         }
       })
       .finally(() => {
@@ -71,6 +78,14 @@ const ExpandedStack = () => {
       >
         {stack.name}
       </TitleSection>
+      <RevisionList
+        revisions={stack.revisions}
+        currentRevision={currentRevision}
+        latestRevision={stack.latest_revision}
+        stackId={stack.id}
+        stackNamespace={namespace}
+        onRevisionClick={(revision) => setCurrentRevision(revision)}
+      ></RevisionList>
       <Br />
       <InfoWrapper>
         <LastDeployed>
@@ -103,26 +118,58 @@ const ExpandedStack = () => {
         </StackErrorMessageStyles.Wrapper>
       ) : null}
 
-      <LineBreak />
-
-      <SortSelector
-        setSortType={setSortType}
-        sortType={sortType}
-        currentView="stacks"
-      />
+      <TabSelector
+        currentTab={currentTab}
+        options={[
+          {
+            label: "Apps",
+            value: "apps",
+            component: (
+              <>
+                <Gap></Gap>
+                {currentRevision.id !== stack.latest_revision.id ? (
+                  <ChartListWrapper>
+                    <Placeholder>
+                      Not available when previewing versions
+                    </Placeholder>
+                  </ChartListWrapper>
+                ) : (
+                  <>
+                    <SortSelector
+                      setSortType={setSortType}
+                      sortType={sortType}
+                      currentView="stacks"
+                    />
 
-      <ChartListWrapper>
-        <ChartList
-          currentCluster={currentCluster}
-          currentView="stacks"
-          namespace={namespace}
-          sortType="Alphabetical"
-          appFilters={
-            stack?.latest_revision?.resources?.map((res) => res.name) || []
-          }
-          closeChartRedirectUrl={`${window.location.pathname}${window.location.search}`}
-        />
-      </ChartListWrapper>
+                    <ChartListWrapper>
+                      <ChartList
+                        currentCluster={currentCluster}
+                        currentView="stacks"
+                        namespace={namespace}
+                        sortType="Alphabetical"
+                        appFilters={
+                          stack?.latest_revision?.resources?.map(
+                            (res) => res.name
+                          ) || []
+                        }
+                        closeChartRedirectUrl={`${window.location.pathname}${window.location.search}`}
+                      />
+                    </ChartListWrapper>
+                  </>
+                )}
+              </>
+            ),
+          },
+          {
+            label: "Source Config",
+            value: "source_config",
+            component: <>Sourrrce configuration</>,
+          },
+        ]}
+        setCurrentTab={(tab) => {
+          setCurrentTab(tab);
+        }}
+      ></TabSelector>
     </div>
   );
 };
@@ -136,6 +183,12 @@ const ChartListWrapper = styled.div`
   padding-bottom: 125px;
 `;
 
+const Gap = styled.div`
+  width: 100%;
+  background: none;
+  height: 30px;
+`;
+
 const StackErrorMessageStyles = {
   Text: styled(Text)`
     font-size: 14px;

+ 255 - 0
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_RevisionList.tsx

@@ -0,0 +1,255 @@
+import Loading from "components/Loading";
+import React, { useContext, useRef, useState } from "react";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { readableDate } from "shared/string_utils";
+import styled from "styled-components";
+import { FullStackRevision, Stack, StackRevision } from "../types";
+
+type RevisionListProps = {
+  revisions: StackRevision[];
+  currentRevision: StackRevision;
+  latestRevision: StackRevision;
+  stackNamespace: string;
+  stackId: string;
+  onRevisionClick: (revision: StackRevision) => void;
+};
+
+const _RevisionList = ({
+  revisions,
+  currentRevision,
+  latestRevision,
+  stackNamespace,
+  stackId,
+  onRevisionClick,
+}: RevisionListProps) => {
+  const { currentProject, currentCluster } = useContext(Context);
+  const [isLoading, setIsLoading] = useState(false);
+  const [isExpanded, setIsExpanded] = useState(false);
+
+  const revisionCache = useRef<{ [id: number]: FullStackRevision }>({});
+
+  const handleRevisionPreview = (revision: StackRevision) => {
+    setIsLoading(true);
+
+    if (revisionCache.current[revision.id]) {
+      onRevisionClick(revisionCache.current[revision.id]);
+      setIsLoading(false);
+      return;
+    }
+
+    api
+      .getStackRevision<FullStackRevision>(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          namespace: stackNamespace,
+          revision_id: revision.id,
+          stack_id: stackId,
+        }
+      )
+      .then((res) => {
+        const newRevision = res.data;
+        revisionCache.current = {
+          ...revisionCache.current,
+          [newRevision.id]: newRevision,
+        };
+        onRevisionClick(newRevision);
+      })
+      .finally(() => {
+        setIsLoading(false);
+      });
+  };
+
+  const handleRevisionRollback = (revision: StackRevision) => {};
+
+  const revisionList = () => {
+    if (revisions.length === 0) {
+      return <div>No revisions</div>;
+    }
+
+    return revisions.map((revision, i) => {
+      let isCurrent = latestRevision.id === revision.id;
+      return (
+        <Tr
+          key={i}
+          onClick={() => handleRevisionPreview(revision)}
+          selected={currentRevision.id === revision.id}
+        >
+          <Td>{revision.id}</Td>
+          <Td>{readableDate(revision.created_at)}</Td>
+          <Td>
+            <RollbackButton
+              disabled={isCurrent}
+              onClick={() => handleRevisionRollback(revision)}
+            >
+              {isCurrent ? "Current" : "Revert"}
+            </RollbackButton>
+          </Td>
+        </Tr>
+      );
+    });
+  };
+
+  return (
+    <>
+      <StyledRevisionSection showRevisions={isExpanded}>
+        {isLoading ? (
+          <LoadingOverlay>
+            <Loading />
+          </LoadingOverlay>
+        ) : null}
+        <RevisionHeader
+          showRevisions={isExpanded}
+          isCurrent={currentRevision.id === latestRevision.id}
+          onClick={() => setIsExpanded((prev) => !prev)}
+        >
+          <RevisionPreview>
+            {currentRevision.id === latestRevision.id
+              ? `Current Revision v${currentRevision.id}`
+              : `Previewing Revision (Not Deployed) v${currentRevision.id}`}
+            <i className="material-icons">arrow_drop_down</i>
+          </RevisionPreview>
+        </RevisionHeader>
+        <TableWrapper>
+          <RevisionsTable>
+            <tbody>
+              <Tr disableHover={true}>
+                <Th>Revision No.</Th>
+                <Th>Timestamp</Th>
+                <Th>Rollback</Th>
+              </Tr>
+              {revisionList()}
+            </tbody>
+          </RevisionsTable>
+        </TableWrapper>
+      </StyledRevisionSection>
+    </>
+  );
+};
+
+export default _RevisionList;
+
+const StyledRevisionSection = styled.div`
+  position: relative;
+  width: 100%;
+  max-height: ${(props: { showRevisions: boolean }) =>
+    props.showRevisions ? "255px" : "40px"};
+  background: #ffffff11;
+  margin: 25px 0px 18px;
+  overflow: hidden;
+  border-radius: 8px;
+  animation: ${(props: { showRevisions: boolean }) =>
+    props.showRevisions ? "expandRevisions 0.3s " : ""};
+  animation-timing-function: "ease-out";
+  @keyframes expandRevisions {
+    from {
+      max-height: 40px;
+    }
+    to {
+      max-height: 250px;
+    }
+  }
+`;
+
+const RevisionHeader = styled.div`
+  color: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
+    props.isCurrent ? "#ffffff66" : "#f5cb42"};
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  height: 40px;
+  font-size: 13px;
+  width: 100%;
+  padding-left: 15px;
+  cursor: pointer;
+  background: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
+    props.showRevisions ? "#ffffff11" : ""};
+  :hover {
+    background: #ffffff18;
+    > div > i {
+      background: #ffffff22;
+    }
+  }
+
+  > div > i {
+    margin-left: 12px;
+    font-size: 20px;
+    cursor: pointer;
+    border-radius: 20px;
+    background: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
+      props.showRevisions ? "#ffffff18" : ""};
+    transform: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
+      props.showRevisions ? "rotate(180deg)" : ""};
+  }
+`;
+
+const RevisionPreview = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const TableWrapper = styled.div`
+  padding-bottom: 20px;
+`;
+
+const RevisionsTable = styled.table`
+  width: 100%;
+  margin-top: 5px;
+  padding-left: 32px;
+  padding-bottom: 20px;
+  min-width: 500px;
+  border-collapse: collapse;
+`;
+const Tr = styled.tr`
+  line-height: 2.2em;
+  cursor: ${(props: { disableHover?: boolean; selected?: boolean }) =>
+    props.disableHover ? "" : "pointer"};
+  background: ${(props: { disableHover?: boolean; selected?: boolean }) =>
+    props.selected ? "#ffffff11" : ""};
+  :hover {
+    background: ${(props: { disableHover?: boolean; selected?: boolean }) =>
+      props.disableHover ? "" : "#ffffff22"};
+  }
+`;
+
+const Td = styled.td`
+  font-size: 13px;
+  color: #ffffff;
+  padding-left: 32px;
+`;
+
+const Th = styled.td`
+  font-size: 13px;
+  font-weight: 500;
+  color: #aaaabb;
+  padding-left: 32px;
+`;
+
+const RollbackButton = styled.div`
+  cursor: ${(props: { disabled: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+  display: flex;
+  border-radius: 3px;
+  align-items: center;
+  justify-content: center;
+  font-weight: 500;
+  height: 21px;
+  font-size: 13px;
+  width: 70px;
+  background: ${(props: { disabled: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
+  :hover {
+    background: ${(props: { disabled: boolean }) =>
+      props.disabled ? "" : "#405eddbb"};
+  }
+`;
+
+const LoadingOverlay = styled.div`
+  background: #43454b90;
+  width: 100%;
+  height: 100%;
+  position: absolute;
+`;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/stacks/routes.tsx

@@ -8,7 +8,7 @@ import {
 } from "react-router";
 import { Context } from "shared/Context";
 import Dashboard from "./Dashboard";
-import ExpandedStack from "./ExpandedStack";
+import ExpandedStack from "./ExpandedStack/ExpandedStack";
 import LaunchRoutes from "./launch";
 
 const routes = () => {

+ 6 - 4
dashboard/src/main/home/cluster-dashboard/stacks/types.ts

@@ -34,10 +34,12 @@ export type Stack = {
 
   revisions: StackRevision[];
 
-  latest_revision: StackRevision & {
-    resources: AppResource[];
-    source_configs: SourceConfig[];
-  };
+  latest_revision: FullStackRevision;
+};
+
+export type FullStackRevision = StackRevision & {
+  resources: AppResource[];
+  source_configs: SourceConfig[];
 };
 
 export type StackRevision = {

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

@@ -1973,7 +1973,7 @@ const getStackRevision = baseApi<
     cluster_id: number;
     namespace: string;
     stack_id: string;
-    revision_id: string;
+    revision_id: number;
   }
 >(
   "GET",