Просмотр исходного кода

Diff modal (#3084)

* Diff View Modal

* Modal View

* Modal View

* Modal View

* Modal View

* Modal View

* Diff Modal Working

* Enable Infra SEttings for non-porter.run emails

* Changes

* Changes

* Changes

* Env Group
sdess09 2 лет назад
Родитель
Сommit
31f9544c7a

+ 29 - 7
dashboard/package-lock.json

@@ -30,6 +30,7 @@
         "ace-builds": "^1.16.0",
         "anser": "^2.0.1",
         "axios": "^0.21.2",
+        "brace": "^0.11.1",
         "chroma-js": "^2.4.2",
         "clipboard": "^2.0.8",
         "color": "^4.2.3",
@@ -40,6 +41,7 @@
         "d3-array": "^2.11.0",
         "d3-time-format": "^3.0.0",
         "dayjs": "^1.11.5",
+        "deep-diff": "^1.0.2",
         "dotenv": "^8.2.0",
         "fuse.js": "^6.6.2",
         "ini": ">=1.3.6",
@@ -51,7 +53,7 @@
         "qs": "^6.9.4",
         "random-word-slugs": "^0.1.6",
         "react": "^18.0.0",
-        "react-ace": "^8.0.0",
+        "react-ace": "^8.1.0",
         "react-animate-height": "^3.1.1",
         "react-color": "^2.19.3",
         "react-datepicker": "^4.8.0",
@@ -4735,6 +4737,11 @@
       "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
       "dev": true
     },
+    "node_modules/brace": {
+      "version": "0.11.1",
+      "resolved": "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz",
+      "integrity": "sha512-Fc8Ne62jJlKHiG/ajlonC4Sd66Pq68fFwK4ihJGNZpGqboc324SQk+lRvMzpPRuJOmfrJefdG8/7JdWX4bzJ2Q=="
+    },
     "node_modules/brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -5950,6 +5957,11 @@
         "node": ">=0.10"
       }
     },
+    "node_modules/deep-diff": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz",
+      "integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg=="
+    },
     "node_modules/deep-equal": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
@@ -10774,9 +10786,9 @@
       }
     },
     "node_modules/react-ace": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-8.0.0.tgz",
-      "integrity": "sha512-EvU14vXbZpAenb1ZVKdn8yTQs/shZ9RghFulHtt67bBXT6sjrNHcfOEXHYtSEmwMb6pQVVNNuulzzd8o+Uouig==",
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-8.1.0.tgz",
+      "integrity": "sha512-n3rm9gRNZjLGlXJQ587RASOQCPn6WlcV2gjRYwvG3gyVpBf4pY6lh/uI9tDkx2zYdEKJUfnGbTmzEGL5yyDWuw==",
       "dependencies": {
         "ace-builds": "^1.4.6",
         "diff-match-patch": "^1.0.4",
@@ -18576,6 +18588,11 @@
       "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
       "dev": true
     },
+    "brace": {
+      "version": "0.11.1",
+      "resolved": "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz",
+      "integrity": "sha512-Fc8Ne62jJlKHiG/ajlonC4Sd66Pq68fFwK4ihJGNZpGqboc324SQk+lRvMzpPRuJOmfrJefdG8/7JdWX4bzJ2Q=="
+    },
     "brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -19587,6 +19604,11 @@
       "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
       "dev": true
     },
+    "deep-diff": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz",
+      "integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg=="
+    },
     "deep-equal": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
@@ -23403,9 +23425,9 @@
       }
     },
     "react-ace": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-8.0.0.tgz",
-      "integrity": "sha512-EvU14vXbZpAenb1ZVKdn8yTQs/shZ9RghFulHtt67bBXT6sjrNHcfOEXHYtSEmwMb6pQVVNNuulzzd8o+Uouig==",
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-8.1.0.tgz",
+      "integrity": "sha512-n3rm9gRNZjLGlXJQ587RASOQCPn6WlcV2gjRYwvG3gyVpBf4pY6lh/uI9tDkx2zYdEKJUfnGbTmzEGL5yyDWuw==",
       "requires": {
         "ace-builds": "^1.4.6",
         "diff-match-patch": "^1.0.4",

+ 3 - 1
dashboard/package.json

@@ -25,6 +25,7 @@
     "ace-builds": "^1.16.0",
     "anser": "^2.0.1",
     "axios": "^0.21.2",
+    "brace": "^0.11.1",
     "chroma-js": "^2.4.2",
     "clipboard": "^2.0.8",
     "color": "^4.2.3",
@@ -35,6 +36,7 @@
     "d3-array": "^2.11.0",
     "d3-time-format": "^3.0.0",
     "dayjs": "^1.11.5",
+    "deep-diff": "^1.0.2",
     "dotenv": "^8.2.0",
     "fuse.js": "^6.6.2",
     "ini": ">=1.3.6",
@@ -46,7 +48,7 @@
     "qs": "^6.9.4",
     "random-word-slugs": "^0.1.6",
     "react": "^18.0.0",
-    "react-ace": "^8.0.0",
+    "react-ace": "^8.1.0",
     "react-animate-height": "^3.1.1",
     "react-color": "^2.19.3",
     "react-datepicker": "^4.8.0",

+ 326 - 0
dashboard/src/main/home/app-dashboard/expanded-app/ChangeLogModal.tsx

@@ -0,0 +1,326 @@
+import React, { useContext, useEffect, useRef, useState } from "react";
+import styled from "styled-components";
+import Modal from "components/porter/Modal";
+import Loading from "components/Loading";
+import Text from "components/porter/Text";
+import yaml from "js-yaml";
+import DiffViewer, { DiffMethod } from "react-diff-viewer";
+import Button from "components/porter/Button";
+import ConfirmOverlay from "components/porter/ConfirmOverlay";
+import Spacer from "components/porter/Spacer";
+import Checkbox from "components/porter/Checkbox";
+import { ChartType } from "shared/types";
+import * as Diff from "deep-diff";
+import api from "shared/api";
+import { Context } from "shared/Context";
+
+type Props = {
+  modalVisible: boolean;
+  setModalVisible: (x: boolean) => void;
+  revision: number;
+  currentChart: ChartType;
+  revertModal?: boolean;
+  appData: any;
+  diffContent: boolean;
+  setDiffContent: (x: boolean) => void;
+};
+
+const ChangeLogModal: React.FC<Props> = ({
+  revision,
+  appData,
+  currentChart,
+  modalVisible,
+  revertModal,
+  setModalVisible,
+}) => {
+  const [values, setValues] = useState("");
+  const [chartEvent, setChartEvent] = useState(null);
+  const [eventValues, setEventValues] = useState("");
+  const [prevChartEvent, setPrevChartEvent] = useState(null);
+  const [prevEventValues, setPrevEventValues] = useState("");
+  const [showRawDiff, setShowRawDiff] = useState(false);
+  const [showOverlay, setShowOverlay] = useState<boolean>(false);
+  const [changesConfig, setChangesConfig] = useState<boolean>(true);
+
+  const [loading, setLoading] = useState(false);
+  const { currentCluster, currentProject, setCurrentError } = useContext(
+    Context
+  );
+  useEffect(() => {
+    let values = "# Nothing here yet";
+    if (currentChart.config) {
+      values = yaml.dump(currentChart.config);
+    }
+    setValues(values);
+  }, [currentChart.config]); // It will run this effect whenever currentChart.config changes
+
+  const getChartData = async (chart: ChartType) => {
+    setLoading(true);
+    const res = await api.getChart(
+      "<token>",
+      {},
+      {
+        name: chart.name,
+        namespace: chart.namespace,
+        cluster_id: currentCluster.id,
+        revision: revision,
+        id: currentProject.id,
+      }
+    );
+    const updatedChart = res.data;
+    setLoading(false);
+    return updatedChart;
+  };
+
+  const revertToRevision = async (revision: number) => {
+    setLoading(true);
+    try {
+      await api
+        .rollbackPorterApp(
+          "<token>",
+          {
+            revision,
+          },
+          {
+            project_id: appData.app.project_id,
+            stack_name: appData.app.name,
+            cluster_id: appData.app.cluster_id,
+          }
+        )
+      window.location.reload();
+    } catch (err) {
+      setLoading(false);
+      console.log(err)
+    }
+  }
+
+  const getPrevChartData = async (chart: ChartType) => {
+    setLoading(true);
+    const prevRevision = revision - 1;
+    const res = await api.getChart(
+      "<token>",
+      {},
+      {
+        name: chart.name,
+        namespace: chart.namespace,
+        cluster_id: currentCluster.id,
+        revision: prevRevision,
+        id: currentProject.id,
+      }
+    );
+    const updatedChart = res.data;
+    setLoading(false);
+    return updatedChart;
+  };
+
+  useEffect(() => {
+    const fetchData = async () => {
+      // Fetch the chart data
+      const updatedChart = await getChartData(currentChart);
+      const prevChart = await getPrevChartData(currentChart);
+
+      // Now that we've waited for getChartData to finish, process the result
+      let eventValues = "# Nothing here yet";
+      if (updatedChart?.config) {
+        eventValues = yaml.dump(updatedChart?.config);
+      }
+      let prevEventValues = "# Nothing here yet";
+      if (prevChart?.config) {
+        prevEventValues = yaml.dump(prevChart?.config);
+      }
+      setEventValues(eventValues);
+      setChartEvent(updatedChart);
+      setPrevEventValues(prevEventValues);
+      setPrevChartEvent(prevChart);
+    };
+
+    fetchData();
+  }, [currentChart.config]);
+
+  const parseYamlAndDisplayDifferences = (oldYaml: any, newYaml: any) => {
+    const diff = Diff.diff(oldYaml, newYaml);
+    const changes: JSX.Element[] = [];
+    // Define the regex pattern to match service creation
+    const servicePattern = /^[a-zA-Z0-9\-]*-[a-zA-Z0-9]*[^\.]$/;
+    diff?.forEach((difference: any) => {
+      let path = difference.path?.join(" ");
+      switch (difference.kind) {
+        case "N":
+          // Check if the added item is a service by testing the path against the regex pattern
+          if (servicePattern.test(path)) {
+            changes.push(<ChangeBox type="N">{`${path} created`}</ChangeBox>);
+          } else {
+            // If not, display the full message
+            changes.push(
+              <ChangeBox type="N">{`${path} added: ${JSON.stringify(
+                difference.rhs
+              )}`}</ChangeBox>
+            );
+          }
+          break;
+        case "D":
+          if (servicePattern.test(path)) {
+            // If so, display a simplified message
+            changes.push(<ChangeBox type="D">
+              {`${path} deleted`}
+            </ChangeBox>);
+          } else {
+
+            changes.push(<ChangeBox type="D">
+              {`${path} removed`}
+            </ChangeBox>);
+          }
+          break;
+        case "E":
+          changes.push(
+            <ChangeBox type="E">
+              {`${path}: ${JSON.stringify(difference.lhs)} -> ${JSON.stringify(
+                difference.rhs
+              )}`}
+            </ChangeBox>
+          );
+          break;
+        case "A":
+          path = path + `[${difference.index}]`;
+          if (difference.item.kind === "N")
+            changes.push(
+              <Text>{`${path} added: ${JSON.stringify(
+                difference.item.rhs
+              )}`}</Text>
+            );
+          if (difference.item.kind === "D")
+            changes.push(<Text>{`${path} removed`}</Text>);
+          if (difference.item.kind === "E")
+            changes.push(
+              <Text>
+                {`${path} updated: ${JSON.stringify(
+                  difference.item.lhs
+                )} -> ${JSON.stringify(difference.item.rhs)}`}
+              </Text>
+            );
+          break;
+      }
+    });
+    if (changes.length === 0) {
+      changes.push(
+        <ChangeBox type="E">
+          {`No changes detected`}
+        </ChangeBox>
+      )
+    }
+
+    return <ChangeLog>{changes}</ChangeLog>
+
+  };
+
+  return (
+    <>
+      <Modal closeModal={() => setModalVisible(false)} width={"800px"}>
+        {revertModal ? <Text size={18}> Revert to version no. {revision} </Text> : <Text size={18}>Changes for version no. {revision}</Text>}
+        {loading ? (
+          <Loading /> // <-- Render loading state
+        ) : (
+          revertModal ? (<>
+            <div style={{ maxHeight: "400px", overflowY: "auto" }}>
+              <DiffViewer
+                leftTitle={revertModal ? `Current Revision` : `Revision No. ${revision - 1}`}
+                rightTitle={`Revision No. ${revision}`}
+                oldValue={revertModal ? values : prevEventValues}
+                newValue={eventValues}
+                splitView={true}
+                hideLineNumbers={false}
+                useDarkTheme={true}
+                compareMethod={DiffMethod.TRIMMED_LINES}
+              />
+            </div>
+          </>) :
+            (<>
+              {showRawDiff ? (
+                <>
+                  <div style={{ maxHeight: "400px", overflowY: "auto", borderRadius: "8px" }}>
+                    <DiffViewer
+                      leftTitle={revertModal ? `Current Revision` : `Revision No. ${revision - 1}`}
+                      rightTitle={`Revision No. ${revision}`}
+                      oldValue={revertModal ? values : prevEventValues}
+                      newValue={eventValues}
+                      splitView={true}
+                      hideLineNumbers={false}
+                      useDarkTheme={true}
+                      compareMethod={DiffMethod.TRIMMED_LINES}
+                    />
+                  </div>
+                </>
+              ) : (
+                <div style={{ maxHeight: "400px", overflowY: "auto" }}>
+                  {revertModal ? parseYamlAndDisplayDifferences(
+                    currentChart.config,
+                    chartEvent?.config
+                  ) : parseYamlAndDisplayDifferences(
+                    prevChartEvent?.config,
+                    chartEvent?.config
+                  )}
+                </div>
+              )}
+
+              {changesConfig && (<><Spacer y={.3} />
+                <div style={{ display: "flex" }}>
+
+                  <Checkbox
+                    checked={showRawDiff}
+                    toggleChecked={() => setShowRawDiff(!showRawDiff)}
+                  >
+                    <Text>Show raw diff</Text>
+                  </Checkbox>
+                </div></>)}
+            </>))
+        }
+
+        {revertModal && (
+          <>
+            <Spacer y={1} />
+            <Button
+              onClick={() => setShowOverlay(true)}
+              width={"110px"}
+              loadingText={"Submitting..."}
+            >
+              Revert
+            </Button>
+          </>
+        )}
+        {showOverlay && (
+
+          <ConfirmOverlay
+            loading={loading}
+            message={`Are you sure you want to revert to version no. ${revision}?`}
+            onYes={() => revertToRevision(revision)}
+            onNo={() => setShowOverlay(false)}
+          />
+
+        )}
+
+      </Modal>
+    </>
+  );
+};
+
+export default ChangeLogModal;
+
+const ChangeLog = styled.div`
+  display: flex;
+  flex-direction: column;
+  border-radius: 8px;
+  overflow: hidden;
+`;
+
+const ChangeBox = styled.div<{ type: string }>`
+  padding: 10px;
+  background-color: ${({ type }) =>
+    type === "N"
+      ? "#034a53"
+      : type === "D"
+        ? "#632f34"
+        : type === "E"
+          ? "#272831"
+          : "#fff"};
+  color: "#fff";
+`;

+ 308 - 0
dashboard/src/main/home/app-dashboard/expanded-app/DiffViewModal.tsx

@@ -0,0 +1,308 @@
+import React, { useEffect, useRef, useState } from "react";
+import styled from "styled-components";
+import Modal from "components/porter/Modal";
+import TitleSection from "components/TitleSection";
+import Loading from "components/Loading";
+import Text from "components/porter/Text";
+import danger from "assets/danger.svg";
+import Anser, { AnserJsonEntry } from "anser";
+import web from "assets/web-bold.png";
+import settings from "assets/settings-bold.png";
+import sliders from "assets/sliders.svg";
+
+import dayjs from "dayjs";
+import Link from "components/porter/Link";
+import Spacer from "components/porter/Spacer";
+import Checkbox from "components/porter/Checkbox";
+import { NavLink } from "react-router-dom";
+import SidebarLink from "main/home/sidebar/SidebarLink";
+import { EnvVariablesTab } from "./EnvVariablesTab";
+type Props = {
+  modalVisible: boolean;
+  setModalVisible: (x: boolean) => void;
+  serviceChild: any;
+};
+
+const DiffViewModal: React.FC<Props> = ({
+  serviceChild,
+  setModalVisible,
+}) => {
+  const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
+  const [currentView, setCurrentView] = useState("overview");
+
+  return (
+    <Modal closeModal={() => setModalVisible(false)} width={"1100px"}>
+      <Text size={18}>Compare Diff</Text>
+
+      <ContentWrapper>
+        <StyledSidebar showSidebar={true}>
+          <SidebarBg />
+          <ScrollWrapper>
+            <NavButton onClick={() => setCurrentView("overview")}>
+              <Img src={web} />
+              Overview
+            </NavButton>
+            <NavButton onClick={() => setCurrentView("environment")}>
+              <Img src={sliders} />
+              Environment
+            </NavButton>
+            <NavButton onClick={() => setCurrentView("buildSettings")}>
+              <Img src={settings} />
+              Build settings
+            </NavButton>
+          </ScrollWrapper>
+        </StyledSidebar>
+
+        <ContentView>
+          {currentView === "overview" && (
+            <ServiceChildContainer>
+              <ServiceChild>
+                <Text> Current </Text>
+                {serviceChild}
+              </ServiceChild>
+              <SidebarBg />
+
+              <ServiceChild>
+                <Text> Revision No.5</Text>
+
+                {serviceChild}
+              </ServiceChild>
+            </ServiceChildContainer>
+          )}
+          {currentView === "environment" && <div></div>}
+          {currentView === "buildSettings" && (
+            <div>
+              <h2>Build Settings</h2>
+              <p>Dummy content for build settings.</p>
+            </div>
+          )}
+        </ContentView>
+      </ContentWrapper>
+    </Modal>
+  );
+};
+
+export default DiffViewModal;
+const ScrollWrapper = styled.div`
+  overflow-y: auto;
+  padding-bottom: 25px;
+  max-height: calc(100vh - 95px);
+`;
+
+const ProjectPlaceholder = styled.div`
+  background: #ffffff11;
+  border-radius: 5px;
+  margin: 0 15px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: calc(100% - 100px);
+  font-size: 13px;
+  color: #aaaabb;
+  padding-bottom: 80px;
+
+  > img {
+    width: 17px;
+    margin-right: 10px;
+  }
+`;
+
+const NavButton = styled(SidebarLink)`
+  display: flex;
+  align-items: center;
+  border-radius: 5px;
+  position: relative;
+  text-decoration: none;
+  height: 34px;
+  margin: 5px 15px;
+  padding: 0 30px 2px 6px;
+  font-size: 13px;
+  color: ${(props) => props.theme.text.primary};
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: any) => (props.active ? "#ffffff11" : "")};
+
+  :hover {
+    background: ${(props: any) => (props.active ? "#ffffff11" : "#ffffff08")};
+  }
+
+  &.active {
+    background: #ffffff11;
+
+    :hover {
+      background: #ffffff11;
+    }
+  }
+
+  :hover {
+    background: #ffffff08;
+  }
+
+  > i {
+    font-size: 18px;
+    border-radius: 3px;
+    margin-left: 2px;
+    margin-right: 10px;
+  }
+`;
+
+const Img = styled.img<{ enlarge?: boolean }>`
+  padding: ${(props) => (props.enlarge ? "0 0 0 1px" : "4px")};
+  height: 22px;
+  padding-top: 4px;
+  border-radius: 3px;
+  margin-right: 8px;
+  opacity: 0.8;
+`;
+
+const SidebarBg = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  background-color: ${(props) => props.theme.bg};
+  height: 100%;
+  z-index: -1;
+  border-right: 1px solid #383a3f;
+`;
+
+const SidebarLabel = styled.div`
+  color: ${(props) => props.theme.text.primary};
+  padding: 5px 23px;
+  margin-bottom: 5px;
+  font-size: 13px;
+  z-index: 1;
+`;
+
+const PullTab = styled.div`
+  position: fixed;
+  width: 30px;
+  height: 50px;
+  background: #7a838f77;
+  top: calc(50vh - 60px);
+  left: 0;
+  z-index: 1;
+  border-top-right-radius: 5px;
+  border-bottom-right-radius: 5px;
+  cursor: pointer;
+
+  :hover {
+    background: #99a5af77;
+  }
+
+  > i {
+    color: #ffffff77;
+    font-size: 18px;
+    position: absolute;
+    top: 15px;
+    left: 4px;
+  }
+`;
+
+const Tooltip = styled.div`
+  position: absolute;
+  right: -60px;
+  top: 34px;
+  min-width: 67px;
+  height: 18px;
+  padding-bottom: 2px;
+  background: #383842dd;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex: 1;
+  color: white;
+  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 CollapseButton = styled.div`
+  position: absolute;
+  right: 0;
+  top: 8px;
+  height: 23px;
+  width: 23px;
+  background: #525563aa;
+  border-top-left-radius: 3px;
+  border-bottom-left-radius: 3px;
+  cursor: pointer;
+
+  :hover {
+    background: #636674;
+  }
+
+  > i {
+    color: #ffffff77;
+    font-size: 14px;
+    transform: rotate(180deg);
+    position: absolute;
+    top: 4px;
+    right: 5px;
+  }
+`;
+
+const StyledSidebar = styled.section`
+  width: 240px;
+  position: relative;
+  padding-top: 20px;
+  height: 75vh;
+  z-index: 2;
+  animation: ${(props: { showSidebar: boolean }) =>
+    props.showSidebar ? "showSidebar 0.4s" : "hideSidebar 0.4s"};
+  animation-fill-mode: forwards;
+  @keyframes showSidebar {
+    from {
+      margin-left: -240px;
+    }
+    to {
+      margin-left: 0px;
+    }
+  }
+  @keyframes hideSidebar {
+    from {
+      margin-left: 0px;
+    }
+    to {
+      margin-left: -240px;
+    }
+  }
+`;
+const ContentView = styled.div`
+  flex 1;
+  overflow: auto;
+  padding: 20px;
+`;
+
+const ContentWrapper = styled.div`
+  display: flex;
+  flex-direction: row;
+  height: 75vh;
+`;
+const ServiceChildContainer = styled.div`
+  display: flex;
+  height: 100%;
+  justify-content: space-between;
+  align-items: flex-start; // align top
+`;
+
+const ServiceChild = styled.div`
+  width: calc(50% - 0.5px);
+`;
+
+const Divider = styled.div`
+  width: 8px;
+
+  background-color: white;
+`;

+ 31 - 34
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/DeployEventCard.tsx

@@ -16,7 +16,7 @@ import styled from "styled-components";
 import Button from "components/porter/Button";
 import api from "shared/api";
 import Link from "components/porter/Link";
-import ConfirmOverlay from "components/porter/ConfirmOverlay";
+import ChangeLogModal from "../../ChangeLogModal";
 
 type Props = {
   event: PorterAppEvent;
@@ -24,8 +24,9 @@ type Props = {
 };
 
 const DeployEventCard: React.FC<Props> = ({ event, appData }) => {
-  const [showOverlay, setShowOverlay] = useState<boolean>(false);
   const [loading, setLoading] = useState<boolean>(false);
+  const [diffModalVisible, setDiffModalVisible] = useState(false);
+  const [revertModalVisible, setRevertModalVisible] = useState(false);
 
   const renderStatusText = (event: PorterAppEvent) => {
     switch (event.status) {
@@ -37,29 +38,6 @@ const DeployEventCard: React.FC<Props> = ({ event, appData }) => {
         return <Text color="#aaaabb66">Deployment in progress...</Text>;
     }
   };
-
-  const revertToRevision = async (revision: number) => {
-    setLoading(true);
-    try {
-      await api
-        .rollbackPorterApp(
-          "<token>",
-          {
-            revision,
-          },
-          {
-            project_id: appData.app.project_id,
-            stack_name: appData.app.name,
-            cluster_id: appData.app.cluster_id,
-          }
-        )
-      window.location.reload();
-    } catch (err) {
-      setLoading(false);
-      console.log(err)
-    }
-  }
-
   return (
     <StyledEventCard>
       <Container row spaced>
@@ -79,22 +57,41 @@ const DeployEventCard: React.FC<Props> = ({ event, appData }) => {
             <>
               <Spacer inline x={1} />
               <TempWrapper>
-                <Link hasunderline onClick={() => setShowOverlay(true)}>
+                <Link hasunderline onClick={() => setRevertModalVisible(true)}>
                   Revert to version {event?.metadata?.revision}
                 </Link>
+
               </TempWrapper>
             </>
           )}
+          <Spacer inline width="15px" />
+          <TempWrapper>
+            {event?.metadata?.revision != 1 && (<Link hasunderline onClick={() => setDiffModalVisible(true)}>
+              View changes
+            </Link>)}
+            {diffModalVisible && (
+              <ChangeLogModal
+                revision={event.metadata.revision}
+                currentChart={appData.chart}
+                modalVisible={diffModalVisible}
+                setModalVisible={setDiffModalVisible}
+                appData={appData}
+              />
+            )}
+            {revertModalVisible && (
+              <ChangeLogModal
+                revision={event.metadata.revision}
+                currentChart={appData.chart}
+                modalVisible={revertModalVisible}
+                setModalVisible={setRevertModalVisible}
+                revertModal={true}
+                appData={appData}
+              />
+            )}
+          </TempWrapper>
         </Container>
       </Container>
-      {showOverlay && (
-        <ConfirmOverlay
-          loading={loading}
-          message={`Are you sure you want to revert to version no. ${event?.metadata?.revision}?`}
-          onYes={() => revertToRevision(event.metadata.revision)}
-          onNo={() => setShowOverlay(false)}
-        />
-      )}
+
     </StyledEventCard>
   );
 };

+ 3 - 1
dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx

@@ -18,6 +18,7 @@ import ReleaseTabs from "./ReleaseTabs";
 interface ServiceProps {
   service: Service;
   chart?: any;
+  readOnly?: boolean;
   editService: (service: Service) => void;
   deleteService: () => void;
   defaultExpanded: boolean;
@@ -27,6 +28,7 @@ interface ServiceProps {
 const ServiceContainer: React.FC<ServiceProps> = ({
   service,
   chart,
+  readOnly,
   deleteService,
   editService,
   defaultExpanded,
@@ -110,7 +112,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
           {renderIcon(service)}
           {service.name.trim().length > 0 ? service.name : "New Service"}
         </ServiceTitle>
-        {service.canDelete && (
+        {service.canDelete && !readOnly && (
           <ActionButton onClick={deleteService}>
             <span className="material-icons">delete</span>
           </ActionButton>

+ 4 - 3
dashboard/src/main/home/app-dashboard/new-app-flow/Services.tsx

@@ -60,7 +60,7 @@ const Services: React.FC<ServicesProps> = ({
     } else {
       return undefined;
     }
-  }
+  };
 
   const maybeRenderAddServicesButton = () => {
     if (limitOne && services.length > 0) {
@@ -87,8 +87,8 @@ const Services: React.FC<ServicesProps> = ({
         </AddServiceButton>
         <Spacer y={0.5} />
       </>
-    )
-  }
+    );
+  };
 
   return (
     <>
@@ -110,6 +110,7 @@ const Services: React.FC<ServicesProps> = ({
                   setServices(newServices);
                 }}
                 defaultExpanded={defaultExpanded}
+                readOnly={true}
               />
             );
           })}