|
|
@@ -5,28 +5,44 @@ import api from "shared/api";
|
|
|
import styled from "styled-components";
|
|
|
import { match } from "ts-pattern";
|
|
|
import loading from "assets/loading.gif";
|
|
|
-import { SourceOptions } from "lib/porter-apps";
|
|
|
+import {
|
|
|
+ PorterAppFormData,
|
|
|
+ SourceOptions,
|
|
|
+ clientAppFromProto,
|
|
|
+} from "lib/porter-apps";
|
|
|
import { z } from "zod";
|
|
|
import { PorterApp } from "@porter-dev/api-contracts";
|
|
|
import { readableDate } from "shared/string_utils";
|
|
|
+import Text from "components/porter/Text";
|
|
|
+import { useLatestRevision } from "./LatestRevisionContext";
|
|
|
+import { useFormContext } from "react-hook-form";
|
|
|
|
|
|
type Props = {
|
|
|
- latestRevisionNumber: number;
|
|
|
deploymentTargetId: string;
|
|
|
projectId: number;
|
|
|
clusterId: number;
|
|
|
appName: string;
|
|
|
- sourceType: SourceOptions["type"];
|
|
|
+ latestSource: SourceOptions;
|
|
|
+ latestRevisionNumber: number;
|
|
|
};
|
|
|
|
|
|
+const RED = "#ff0000";
|
|
|
+const YELLOW = "#FFA500";
|
|
|
+
|
|
|
const RevisionsList: React.FC<Props> = ({
|
|
|
latestRevisionNumber,
|
|
|
deploymentTargetId,
|
|
|
projectId,
|
|
|
clusterId,
|
|
|
appName,
|
|
|
- sourceType,
|
|
|
+ latestSource,
|
|
|
}) => {
|
|
|
+ const {
|
|
|
+ previewRevision,
|
|
|
+ setPreviewRevision,
|
|
|
+ servicesFromYaml,
|
|
|
+ } = useLatestRevision();
|
|
|
+ const { reset } = useFormContext<PorterAppFormData>();
|
|
|
const [expandRevisions, setExpandRevisions] = useState(false);
|
|
|
|
|
|
const res = useQuery(
|
|
|
@@ -54,6 +70,58 @@ const RevisionsList: React.FC<Props> = ({
|
|
|
}
|
|
|
);
|
|
|
|
|
|
+ const getReadableStatus = (status: AppRevision["status"]) =>
|
|
|
+ match(status)
|
|
|
+ .with("CREATED", () => "Created")
|
|
|
+ .with("AWAITING_BUILD_ARTIFACT", () => "Awaiting Build")
|
|
|
+ .with("READY_TO_APPLY", () => "Deploying")
|
|
|
+ .with("AWAITING_PREDEPLOY", () => "Awaiting Pre-Deploy")
|
|
|
+ .with("BUILD_CANCELED", () => "Build Canceled")
|
|
|
+ .with("BUILD_FAILED", () => "Build Failed")
|
|
|
+ .with("DEPLOY_FAILED", () => "Deploy Failed")
|
|
|
+ .with("DEPLOYED", () => "Deployed")
|
|
|
+ .exhaustive();
|
|
|
+
|
|
|
+ const getDotColor = (status: AppRevision["status"]) =>
|
|
|
+ match(status)
|
|
|
+ .with(
|
|
|
+ "CREATED",
|
|
|
+ "AWAITING_BUILD_ARTIFACT",
|
|
|
+ "READY_TO_APPLY",
|
|
|
+ "AWAITING_PREDEPLOY",
|
|
|
+ () => YELLOW
|
|
|
+ )
|
|
|
+ .otherwise(() => RED);
|
|
|
+
|
|
|
+ const getTableHeader = (latestRevision?: AppRevision) => {
|
|
|
+ if (!latestRevision) {
|
|
|
+ return "Revisions";
|
|
|
+ }
|
|
|
+
|
|
|
+ if (previewRevision) {
|
|
|
+ return "Previewing revision (not deployed) -";
|
|
|
+ }
|
|
|
+
|
|
|
+ return "Current revision - ";
|
|
|
+ };
|
|
|
+
|
|
|
+ const getSelectedRevisionNumber = (args: {
|
|
|
+ numDeployed: number;
|
|
|
+ latestRevision?: AppRevision;
|
|
|
+ }) => {
|
|
|
+ const { numDeployed, latestRevision } = args;
|
|
|
+
|
|
|
+ if (previewRevision) {
|
|
|
+ return previewRevision;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (latestRevision && latestRevision.revision_number !== 0) {
|
|
|
+ return latestRevision.revision_number;
|
|
|
+ }
|
|
|
+
|
|
|
+ return numDeployed + 1;
|
|
|
+ };
|
|
|
+
|
|
|
const renderContents = (revisions: AppRevision[]) => {
|
|
|
const revisionsWithProto = revisions.map((revision) => {
|
|
|
return {
|
|
|
@@ -62,19 +130,34 @@ const RevisionsList: React.FC<Props> = ({
|
|
|
};
|
|
|
});
|
|
|
|
|
|
+ const deployedRevisions = revisionsWithProto.filter(
|
|
|
+ (r) => r.revision_number !== 0
|
|
|
+ );
|
|
|
+ const pendingRevisions = revisionsWithProto.filter(
|
|
|
+ (r) => r.revision_number === 0
|
|
|
+ );
|
|
|
+
|
|
|
return (
|
|
|
<div>
|
|
|
<RevisionHeader
|
|
|
showRevisions={expandRevisions}
|
|
|
- isCurrent
|
|
|
+ isCurrent={!previewRevision}
|
|
|
onClick={() => {
|
|
|
setExpandRevisions((prev) => !prev);
|
|
|
}}
|
|
|
>
|
|
|
<RevisionPreview>
|
|
|
<i className="material-icons">arrow_drop_down</i>
|
|
|
- Current version -{" "}
|
|
|
- <Revision>No. {revisions[0].revision_number}</Revision>
|
|
|
+ {getTableHeader(revisions[0])}
|
|
|
+ {revisions[0] ? (
|
|
|
+ <Revision>
|
|
|
+ No.{" "}
|
|
|
+ {getSelectedRevisionNumber({
|
|
|
+ numDeployed: deployedRevisions.length,
|
|
|
+ latestRevision: revisions[0],
|
|
|
+ })}
|
|
|
+ </Revision>
|
|
|
+ ) : null}
|
|
|
</RevisionPreview>
|
|
|
</RevisionHeader>
|
|
|
<RevisionList>
|
|
|
@@ -83,37 +166,93 @@ const RevisionsList: React.FC<Props> = ({
|
|
|
<tbody>
|
|
|
<Tr disableHover>
|
|
|
<Th>Revision no.</Th>
|
|
|
- <Th>Timestamp</Th>
|
|
|
<Th>
|
|
|
{revisionsWithProto[0]?.app_proto.build
|
|
|
? "Commit SHA"
|
|
|
: "Image Tag"}
|
|
|
</Th>
|
|
|
+ <Th>Timestamp</Th>
|
|
|
+ <Th>Status</Th>
|
|
|
<Th>Rollback</Th>
|
|
|
</Tr>
|
|
|
- {revisionsWithProto.map((revision) => (
|
|
|
- <Tr key={revision.revision_number}>
|
|
|
- <Td>{revision.revision_number}</Td>
|
|
|
- <Td>{readableDate(revision.updated_at)}</Td>
|
|
|
- <Td>
|
|
|
- {revision.app_proto.build
|
|
|
- ? revision.app_proto.build.commitSha.substring(0, 7)
|
|
|
- : revision.app_proto.image?.tag}
|
|
|
- </Td>
|
|
|
- <Td>
|
|
|
- <RollbackButton
|
|
|
- disabled={
|
|
|
- revision.revision_number === latestRevisionNumber
|
|
|
- }
|
|
|
- onClick={() => {}}
|
|
|
- >
|
|
|
- {revision.revision_number === latestRevisionNumber
|
|
|
- ? "Current"
|
|
|
- : "Revert"}
|
|
|
- </RollbackButton>
|
|
|
- </Td>
|
|
|
- </Tr>
|
|
|
- ))}
|
|
|
+ {pendingRevisions.length > 0 &&
|
|
|
+ pendingRevisions.map((revision) => (
|
|
|
+ <Tr key={new Date(revision.updated_at).toUTCString()}>
|
|
|
+ <Td>{deployedRevisions.length + 1}</Td>
|
|
|
+ <Td>
|
|
|
+ {revision.app_proto.build
|
|
|
+ ? revision.app_proto.build.commitSha.substring(0, 7)
|
|
|
+ : revision.app_proto.image?.tag}
|
|
|
+ </Td>
|
|
|
+ <Td>{readableDate(revision.updated_at)}</Td>
|
|
|
+ <Td>
|
|
|
+ <StatusContainer>
|
|
|
+ <Text>{getReadableStatus(revision.status)}</Text>
|
|
|
+ <StatusDot color={getDotColor(revision.status)} />
|
|
|
+ </StatusContainer>
|
|
|
+ </Td>
|
|
|
+ <Td>-</Td>
|
|
|
+ </Tr>
|
|
|
+ ))}
|
|
|
+
|
|
|
+ {deployedRevisions.map((revision, i) => {
|
|
|
+ const isLatestDeployedRevision =
|
|
|
+ latestRevisionNumber !== 0
|
|
|
+ ? revision.revision_number === latestRevisionNumber
|
|
|
+ : i === 0;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Tr
|
|
|
+ key={revision.revision_number}
|
|
|
+ selected={
|
|
|
+ previewRevision
|
|
|
+ ? revision.revision_number === previewRevision
|
|
|
+ : isLatestDeployedRevision
|
|
|
+ }
|
|
|
+ onClick={() => {
|
|
|
+ reset({
|
|
|
+ app: clientAppFromProto(
|
|
|
+ revision.app_proto,
|
|
|
+ servicesFromYaml
|
|
|
+ ),
|
|
|
+ source: latestSource,
|
|
|
+ });
|
|
|
+ setPreviewRevision(
|
|
|
+ isLatestDeployedRevision
|
|
|
+ ? null
|
|
|
+ : revision.revision_number
|
|
|
+ );
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Td>{revision.revision_number}</Td>
|
|
|
+
|
|
|
+ <Td>
|
|
|
+ {revision.app_proto.build
|
|
|
+ ? revision.app_proto.build.commitSha.substring(0, 7)
|
|
|
+ : revision.app_proto.image?.tag}
|
|
|
+ </Td>
|
|
|
+ <Td>{readableDate(revision.updated_at)}</Td>
|
|
|
+ <Td>
|
|
|
+ {!isLatestDeployedRevision ? (
|
|
|
+ getReadableStatus(revision.status)
|
|
|
+ ) : (
|
|
|
+ <StatusContainer>
|
|
|
+ <Text>{getReadableStatus(revision.status)}</Text>
|
|
|
+ <StatusDot />
|
|
|
+ </StatusContainer>
|
|
|
+ )}
|
|
|
+ </Td>
|
|
|
+ <Td>
|
|
|
+ <RollbackButton
|
|
|
+ disabled={isLatestDeployedRevision}
|
|
|
+ onClick={() => {}}
|
|
|
+ >
|
|
|
+ {isLatestDeployedRevision ? "Current" : "Revert"}
|
|
|
+ </RollbackButton>
|
|
|
+ </Td>
|
|
|
+ </Tr>
|
|
|
+ );
|
|
|
+ })}
|
|
|
</tbody>
|
|
|
</RevisionsTable>
|
|
|
</TableWrapper>
|
|
|
@@ -291,3 +430,37 @@ const RollbackButton = styled.div`
|
|
|
props.disabled ? "" : "#405eddbb"};
|
|
|
}
|
|
|
`;
|
|
|
+
|
|
|
+const StatusContainer = styled.div`
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+`;
|
|
|
+
|
|
|
+const StatusDot = styled.div<{ color?: string }>`
|
|
|
+ min-width: 7px;
|
|
|
+ max-width: 7px;
|
|
|
+ height: 7px;
|
|
|
+ margin-left: 10px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: ${(props) => props.color || "#38a88a"};
|
|
|
+
|
|
|
+ box-shadow: 0 0 0 0 rgba(0, 0, 0, 1);
|
|
|
+ transform: scale(1);
|
|
|
+ animation: pulse 2s infinite;
|
|
|
+ @keyframes pulse {
|
|
|
+ 0% {
|
|
|
+ transform: scale(0.95);
|
|
|
+ box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.9);
|
|
|
+ }
|
|
|
+
|
|
|
+ 70% {
|
|
|
+ transform: scale(1);
|
|
|
+ box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ 100% {
|
|
|
+ transform: scale(0.95);
|
|
|
+ box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|