Quellcode durchsuchen

Fix revision history rollback in expanded app (#3306)

Feroze Mohideen vor 2 Jahren
Ursprung
Commit
3cf2c3cf35

+ 6 - 6
api/server/handlers/porter_app/create.go

@@ -282,15 +282,15 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	} else {
 		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "upgrading-application", Value: true})
 
-		// create/update the release job chart
+		// create/update the pre-deploy job chart
 		if request.OverrideRelease {
 			if preDeployJobValues == nil {
-				releaseJobName := fmt.Sprintf("%s-r", stackName)
-				_, err := helmAgent.GetRelease(ctx, releaseJobName, 0, false)
+				preDeployJobName := fmt.Sprintf("%s-r", stackName)
+				_, err := helmAgent.GetRelease(ctx, preDeployJobName, 0, false)
 				if err == nil {
 					// handle exception where the user has chosen to delete the release job
 					telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deleting-pre-deploy-job", Value: true})
-					_, err = helmAgent.UninstallChart(ctx, releaseJobName)
+					_, err = helmAgent.UninstallChart(ctx, preDeployJobName)
 					if err != nil {
 						err = telemetry.Error(ctx, span, err, "error uninstalling pre-deploy job chart")
 						c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
@@ -298,8 +298,8 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 					}
 				}
 			} else {
-				releaseJobName := fmt.Sprintf("%s-r", stackName)
-				helmRelease, err := helmAgent.GetRelease(ctx, releaseJobName, 0, false)
+				preDeployJobName := fmt.Sprintf("%s-r", stackName)
+				helmRelease, err := helmAgent.GetRelease(ctx, preDeployJobName, 0, false)
 				if err != nil {
 					telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "creating-pre-deploy-job", Value: true})
 					conf, err := createReleaseJobChart(

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

@@ -123,6 +123,9 @@ func (p *CreateUpdatePorterAppEventHandler) createNewAppEvent(ctx context.Contex
 	if err != nil {
 		return types.PorterAppEvent{}, telemetry.Error(ctx, span, err, "error retrieving porter app by name for cluster")
 	}
+	if app == nil || app.ID == 0 {
+		return types.PorterAppEvent{}, telemetry.Error(ctx, span, nil, "porter app not found")
+	}
 	telemetry.WithAttributes(span,
 		telemetry.AttributeKV{Key: "porter-app-id", Value: app.ID},
 		telemetry.AttributeKV{Key: "porter-app-name", Value: porterAppName},

+ 71 - 6
api/server/handlers/porter_app/rollback.go

@@ -3,6 +3,7 @@ package porter_app
 import (
 	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -11,8 +12,10 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/telemetry"
+	"gopkg.in/yaml.v2"
 )
 
 type RollbackPorterAppHandler struct {
@@ -59,14 +62,35 @@ func (c *RollbackPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	helmRelease, err := helmAgent.GetRelease(ctx, stackName, 0, false)
+	k8sAgent, err := c.GetAgent(r, cluster, namespace)
 	if err != nil {
-		err = telemetry.Error(ctx, span, err, "error getting helm release")
+		err = telemetry.Error(ctx, span, err, "error getting k8s agent")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
 
-	imageInfo := attemptToGetImageInfoFromRelease(helmRelease.Config)
+	helmReleaseFromRequestedRevision, err := helmAgent.GetRelease(ctx, stackName, request.Revision, false)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting helm release for requested revision")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	latestHelmRelease, err := helmAgent.GetRelease(ctx, stackName, 0, false)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting latest helm release")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	valuesYaml, err := yaml.Marshal(helmReleaseFromRequestedRevision.Config)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error marshalling helm release config to yaml")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	imageInfo := attemptToGetImageInfoFromRelease(helmReleaseFromRequestedRevision.Config)
 	if imageInfo.Tag == "" {
 		imageInfo.Tag = "latest"
 	}
@@ -77,15 +101,56 @@ func (c *RollbackPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
+	injectLauncher := strings.Contains(porterApp.Builder, "heroku") ||
+		strings.Contains(porterApp.Builder, "paketo")
+
+	registries, err := c.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error listing registries")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
 
-	err = helmAgent.RollbackRelease(ctx, helmRelease.Name, request.Revision)
+	chart, values, _, err := parse(
+		ParseConf{
+			ImageInfo:    imageInfo,
+			ServerConfig: c.Config(),
+			ProjectID:    cluster.ProjectID,
+			Namespace:    namespace,
+			SubdomainCreateOpts: SubdomainCreateOpts{
+				k8sAgent:       k8sAgent,
+				dnsRepo:        c.Repo().DNSRecord(),
+				powerDnsClient: c.Config().PowerDNSClient,
+				appRootDomain:  c.Config().ServerConf.AppRootDomain,
+				stackName:      stackName,
+			},
+			InjectLauncherToStartCommand: injectLauncher,
+			FullHelmValues:               string(valuesYaml),
+		},
+	)
 	if err != nil {
-		err = telemetry.Error(ctx, span, err, "error rolling back release")
+		err = telemetry.Error(ctx, span, err, "error parsing helm chart")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
 
-	_, err = createPorterAppEvent(ctx, "SUCCESS", porterApp.ID, helmRelease.Version+1, imageInfo.Tag, c.Repo().PorterAppEvent())
+	conf := &helm.InstallChartConfig{
+		Chart:      chart,
+		Name:       stackName,
+		Namespace:  namespace,
+		Values:     values,
+		Cluster:    cluster,
+		Repo:       c.Repo(),
+		Registries: registries,
+	}
+	_, err = helmAgent.UpgradeInstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error upgrading application")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	_, err = createPorterAppEvent(ctx, "SUCCESS", porterApp.ID, latestHelmRelease.Version+1, imageInfo.Tag, c.Repo().PorterAppEvent())
 	if err != nil {
 		err = telemetry.Error(ctx, span, err, "error creating porter app event")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))

+ 4 - 2
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -26,7 +26,6 @@ import Back from "components/porter/Back";
 import TabSelector from "components/TabSelector";
 import Icon from "components/porter/Icon";
 import { ChartType, CreateUpdatePorterAppOptions } from "shared/types";
-import RevisionSection from "main/home/cluster-dashboard/expanded-chart/RevisionSection";
 import BuildSettingsTab from "../build-settings/BuildSettingsTab";
 import Button from "components/porter/Button";
 import Services from "../new-app-flow/Services";
@@ -49,6 +48,7 @@ import { PartialEnvGroup, PopulatedEnvGroup } from "../../../../components/porte
 import { BuildMethod, PorterApp } from "../types/porterApp";
 import HelmValuesTab from "./HelmValuesTab";
 import ProjectDeleteConsent from "main/home/project-settings/ProjectDeleteConsent";
+import PorterAppRevisionSection from "./PorterAppRevisionSection";
 
 type Props = RouteComponentProps & {};
 
@@ -913,7 +913,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
               ) : (
                 <>
                   <DarkMatter />
-                  <RevisionSection
+                  <PorterAppRevisionSection
                     showRevisions={showRevisions}
                     toggleShowRevisions={() => {
                       setShowRevisions(!showRevisions);
@@ -927,7 +927,9 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                       appData.chart.latest_version !==
                       appData.chart.chart.metadata.version
                     }
+                    updatePorterApp={updatePorterApp}
                     latestVersion={appData.chart.latest_version}
+                    appName={appData.app.name}
                   />
                   <DarkMatter antiHeight="-18px" />
                 </>

+ 550 - 0
dashboard/src/main/home/app-dashboard/expanded-app/PorterAppRevisionSection.tsx

@@ -0,0 +1,550 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+import loading from "assets/loading.gif";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { ChartType, CreateUpdatePorterAppOptions, StorageType } from "shared/types";
+
+import ConfirmOverlay from "components/ConfirmOverlay";
+import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
+
+import Modal from "main/home/modals/Modal";
+import UpgradeChartModal from "main/home/modals/UpgradeChartModal";
+import { readableDate } from "shared/string_utils";
+import { createPortal } from "react-dom";
+import yaml from "js-yaml";
+
+type PropsType = WithAuthProps & {
+    chart: ChartType;
+    refreshChart: () => void;
+    setRevision: (x: ChartType, isCurrent?: boolean) => void;
+    forceRefreshRevisions: boolean;
+    refreshRevisionsOff: () => void;
+    shouldUpdate: boolean;
+    upgradeVersion: (version: string, cb: () => void) => void;
+    latestVersion: string;
+    showRevisions?: boolean;
+    toggleShowRevisions?: () => void;
+    updatePorterApp: (options: Partial<CreateUpdatePorterAppOptions>) => Promise<void>;
+    appName: string;
+};
+
+type StateType = {
+    revisions: ChartType[];
+    rollbackRevision: number | null;
+    upgradeVersion: string;
+    loading: boolean;
+    maxVersion: number;
+    expandRevisions: boolean;
+};
+
+// TODO: refactor this component it's so gross
+class PorterAppRevisionSection extends Component<PropsType, StateType> {
+    state = {
+        revisions: [] as ChartType[],
+        rollbackRevision: null as number | null,
+        upgradeVersion: "",
+        loading: false,
+        maxVersion: 0, // Track most recent version even when previewing old revisions
+        expandRevisions: false,
+    };
+
+    ws: WebSocket | null = null;
+
+    refreshHistory = () => {
+        let { chart } = this.props;
+        let { currentCluster, currentProject } = this.context;
+
+        return api
+            .getRevisions(
+                "<token>",
+                {},
+                {
+                    id: currentProject.id,
+                    namespace: chart.namespace,
+                    cluster_id: currentCluster.id,
+                    name: chart.name,
+                }
+            )
+            .then((res) => {
+                res.data.sort((a: ChartType, b: ChartType) => {
+                    return -(a.version - b.version);
+                });
+                this.setState({
+                    revisions: res.data,
+                    maxVersion: res.data[0].version,
+                });
+            })
+            .catch(console.log);
+    };
+
+    componentDidMount() {
+        this.refreshHistory();
+        this.connectToLiveUpdates();
+    }
+
+    componentWillUnmount() {
+        if (this.ws) {
+            this.ws.close(); // Close the WebSocket connection
+        }
+    }
+
+    connectToLiveUpdates() {
+        let { chart } = this.props;
+        let { currentCluster, currentProject } = this.context;
+
+        const apiPath = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/helm_release?charts=${chart.name}`;
+        const protocol = window.location.protocol == "https:" ? "wss" : "ws";
+        const url = `${protocol}://${window.location.host}`;
+
+        this.ws = new WebSocket(`${url}${apiPath}`);
+
+        this.ws.onopen = () => {
+            console.log("connected to chart live updates websocket");
+        };
+
+        this.ws.onmessage = (evt: MessageEvent) => {
+            let event = JSON.parse(evt.data);
+
+            if (event.event_type == "UPDATE") {
+                let object = event.Object;
+
+                this.setState(
+                    (prevState) => {
+                        const { revisions: oldRevisions } = prevState;
+                        // Copy old array to clean up references
+                        const prevRevisions = [...oldRevisions];
+
+                        // Check if it's an update of a revision or if it's a new one
+                        const revisionIndex = prevRevisions.findIndex((rev) => {
+                            if (rev.version === object.version) {
+                                return true;
+                            }
+                        });
+
+                        // Place new one at top of the array or update the old one
+                        if (revisionIndex > -1) {
+                            prevRevisions.splice(revisionIndex, 1, object);
+                        } else {
+                            return { ...prevState, revisions: [object, ...prevRevisions] };
+                        }
+
+                        return { ...prevState, revisions: prevRevisions, maxVersion: Math.max(...prevRevisions.map(rev => rev.version)) };
+                    },
+                    () => {
+                        this.props.setRevision(this.state.revisions[0], true);
+                    }
+                );
+            }
+        };
+
+        this.ws.onclose = () => {
+            console.log("closing chart live updates websocket");
+        };
+
+        this.ws.onerror = (err: ErrorEvent) => {
+            console.log(err);
+            this.ws.close();
+        };
+    }
+
+    // Handle update of values.yaml
+    componentDidUpdate(prevProps: PropsType) {
+        console.log('calling component did update')
+        if (this.props.forceRefreshRevisions) {
+            console.log("force refresh revisions")
+            this.props.refreshRevisionsOff();
+
+            // Force refresh occurs on submit -> set current to newest
+            this.refreshHistory().then(() => {
+                this.props.setRevision(this.state.revisions[0], true);
+            });
+        } else if (this.props.chart !== prevProps.chart) {
+            console.log("refresh history")
+            this.refreshHistory();
+        }
+    }
+
+    handleRollback = async () => {
+        let { setCurrentError, currentCluster, currentProject } = this.context;
+
+        let revisionNumber = this.state.rollbackRevision;
+        if (revisionNumber == null) {
+            return;
+        }
+        this.setState({ loading: true, rollbackRevision: null });
+
+        try {
+            await api.rollbackPorterApp(
+                "<token>",
+                {
+                    revision: revisionNumber,
+                },
+                {
+                    project_id: currentProject.id,
+                    cluster_id: currentCluster.id,
+                    stack_name: this.props.appName,
+                }
+            );
+        } catch {
+            // TODO: handle error better
+            setCurrentError(err.response.data);
+        } finally {
+            this.setState({ loading: false });
+        }
+    };
+
+    handleClickRevision = (revision: ChartType) => {
+        this.props.setRevision(
+            revision,
+            revision.version === this.state.maxVersion
+        );
+    };
+
+    renderRevisionList = () => {
+        return this.state.revisions.map((revision: ChartType, i: number) => {
+            let isCurrent = revision.version === this.state.maxVersion;
+            const isGithubApp = !!this.props.chart.git_action_config;
+            const imageTag = revision.config?.image?.tag || revision.config?.global?.image?.tag;
+
+            const parsedImageTag = isGithubApp
+                ? String(imageTag).slice(0, 7)
+                : imageTag;
+
+            const isStack = !!this.props.chart.stack_id;
+
+            return (
+                <Tr
+                    key={i}
+                    onClick={() => this.handleClickRevision(revision)}
+                    selected={this.props.chart.version === revision.version}
+                >
+                    <Td>{revision.version}</Td>
+                    <Td>{readableDate(revision.info.last_deployed)}</Td>
+                    <Td>
+                        {!imageTag ? (
+                            "N/A"
+                        ) : isGithubApp && /^[0-9A-Fa-f]{7}$/g.test(imageTag) ? (
+                            <A
+                                href={`https://github.com/${this.props.chart.git_action_config?.git_repo}/commit/${imageTag}`}
+                                target="_blank"
+                                onClick={(e) => {
+                                    e.stopPropagation();
+                                }}
+                            >
+                                {parsedImageTag}
+                            </A>
+                        ) : (
+                            parsedImageTag
+                        )}
+                    </Td>
+                    <Td>v{revision.chart.metadata.version}</Td>
+                    <Td>
+                        <RollbackButton
+                            disabled={
+                                isCurrent ||
+                                !this.props.isAuthorized("application", "", [
+                                    "get",
+                                    "update",
+                                ]) ||
+                                isStack
+                            }
+                            onClick={(e) => {
+                                e.stopPropagation();
+                                this.setState({ rollbackRevision: revision.version })
+                            }
+                            }
+                        >
+                            {isCurrent ? "Current" : "Revert"}
+                        </RollbackButton>
+                    </Td>
+                </Tr >
+            );
+        });
+    };
+
+    renderExpanded = () => {
+        if (this.state.expandRevisions) {
+            return (
+                <TableWrapper>
+                    <RevisionsTable>
+                        <tbody>
+                            <Tr disableHover={true}>
+                                <Th>Revision no.</Th>
+                                <Th>Timestamp</Th>
+                                <Th>
+                                    {this.props.chart.git_action_config ? "Commit" : "Image Tag"}
+                                </Th>
+                                <Th>Template version</Th>
+                                <Th>Rollback</Th>
+                            </Tr>
+                            {this.renderRevisionList()}
+                        </tbody>
+                    </RevisionsTable>
+                </TableWrapper>
+            );
+        }
+    };
+
+    renderContents = () => {
+        if (this.state.loading) {
+            return (
+                <LoadingPlaceholder>
+                    <StatusWrapper>
+                        <LoadingGif src={loading} revision={false} /> Updating . . .
+                    </StatusWrapper>
+                </LoadingPlaceholder>
+            );
+        }
+
+        let isCurrent =
+            this.props.chart.version === this.state.maxVersion ||
+            this.state.maxVersion === 0;
+        return (
+            <div>
+                {this.state.upgradeVersion && (
+                    <Modal
+                        onRequestClose={() => this.setState({ upgradeVersion: "" })}
+                        width="500px"
+                        height="450px"
+                    >
+                        <UpgradeChartModal
+                            currentChart={this.props.chart}
+                            closeModal={() => {
+                                this.setState({ upgradeVersion: "" });
+                            }}
+                            onSubmit={() => {
+                                this.props.upgradeVersion(this.state.upgradeVersion, () => {
+                                    this.setState({ loading: false });
+                                });
+                                this.setState({ upgradeVersion: "", loading: true });
+                            }}
+                        />
+                    </Modal>
+                )}
+                <RevisionHeader
+                    showRevisions={this.props.showRevisions}
+                    isCurrent={isCurrent}
+                    onClick={() => {
+                        if (typeof this.props.toggleShowRevisions === "function") {
+                            this.props.toggleShowRevisions();
+                        }
+                        this.setState((prev) => ({
+                            ...prev,
+                            expandRevisions: !prev.expandRevisions,
+                        }));
+                    }}
+                >
+                    <RevisionPreview>
+                        <i className="material-icons">arrow_drop_down</i>
+                        {isCurrent
+                            ? `Current version`
+                            : `Previewing revision (not deployed)`}{" "}
+                        - <Revision>No. {this.props.chart.version}</Revision>
+                    </RevisionPreview>
+                </RevisionHeader>
+                <RevisionList>{this.renderExpanded()}</RevisionList>
+            </div>
+        );
+    };
+
+    render() {
+        return (
+            <StyledRevisionSection showRevisions={this.state.expandRevisions}>
+                {this.renderContents()}
+                {createPortal(
+                    <ConfirmOverlay
+                        show={this.state.rollbackRevision != null}
+                        message={`Are you sure you want to revert to version ${this.state.rollbackRevision}?`}
+                        onYes={this.handleRollback}
+                        onNo={() => this.setState({ rollbackRevision: null })}
+                    />,
+                    document.body
+                )}
+            </StyledRevisionSection>
+        );
+    }
+}
+
+PorterAppRevisionSection.contextType = Context;
+
+export default withAuth(PorterAppRevisionSection);
+
+const TableWrapper = styled.div`
+  padding-bottom: 20px;
+`;
+
+const LoadingPlaceholder = styled.div`
+  height: 40px;
+  display: flex;
+  align-items: center;
+  padding-left: 20px;
+`;
+
+const LoadingGif = styled.img`
+  width: 15px;
+  height: 15px;
+  margin-right: ${(props: { revision: boolean }) =>
+        props.revision ? "0px" : "9px"};
+  margin-left: ${(props: { revision: boolean }) =>
+        props.revision ? "10px" : "0px"};
+  margin-bottom: ${(props: { revision: boolean }) =>
+        props.revision ? "-2px" : "0px"};
+`;
+
+const StatusWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  font-family: "Work Sans", sans-serif;
+  font-size: 13px;
+  color: #ffffff55;
+  margin-right: 25px;
+`;
+
+const RevisionList = styled.div`
+  overflow-y: auto;
+  max-height: 215px;
+`;
+
+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 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 RevisionsTable = styled.table`
+  width: 100%;
+  margin-top: 5px;
+  padding-left: 32px;
+  padding-bottom: 20px;
+  min-width: 500px;
+  border-collapse: collapse;
+`;
+
+const Revision = styled.div`
+  color: #ffffff;
+  margin-left: 5px;
+`;
+
+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: 10px;
+  cursor: pointer;
+  background: ${({ theme }) => theme.fg};
+  :hover {
+    background: ${(props) => props.showRevisions && props.theme.fg2};
+  }
+
+  > div > i {
+    margin-right: 8px;
+    font-size: 20px;
+    cursor: pointer;
+    border-radius: 20px;
+    transform: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
+        props.showRevisions ? "" : "rotate(-90deg)"};
+  }
+`;
+
+const StyledRevisionSection = styled.div`
+  width: 100%;
+  max-height: ${(props: { showRevisions: boolean }) =>
+        props.showRevisions ? "255px" : "40px"};
+  margin: 20px 0px 18px;
+  overflow: hidden;
+  border-radius: 5px;
+  background: ${props => props.theme.fg};
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+  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 RevisionPreview = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const RevisionUpdateMessage = styled.div`
+  color: white;
+  display: flex;
+  align-items: center;
+  padding: 4px 10px;
+  border-radius: 5px;
+  margin-right: 10px;
+
+  :hover {
+    border: 1px solid white;
+    padding: 3px 9px;
+  }
+
+  > i {
+    margin-right: 6px;
+    font-size: 20px;
+    cursor: pointer;
+    border-radius: 20px;
+    transform: none;
+  }
+`;
+
+const A = styled.a`
+  color: #8590ff;
+  text-decoration: underline;
+  cursor: pointer;
+`;