Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve pending tx modal + shell/download file error handling #36

Merged
merged 8 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { LeaseShellCode } from "@src/types/shell";
import { useCustomWebSocket } from "@src/hooks/useCustomWebSocket";
import { LeaseDto } from "@src/types/deployment";
import { useProviderList } from "@src/queries/useProvidersQuery";
import Link from "next/link";
import LaunchIcon from "@mui/icons-material/Launch";
import { UrlService } from "@src/utils/urlUtils";

type Props = {
leases: LeaseDto[];
Expand All @@ -28,6 +31,7 @@ export const DeploymentLeaseShell: React.FunctionComponent<Props> = ({ leases })
const [selectedLease, setSelectedLease] = useState<LeaseDto>(null);
const [isShowingDownloadModal, setIsShowingDownloadModal] = useState(false);
const [isChangingSocket, setIsChangingSocket] = useState(false);
const [showArrowAndTabWarning, setShowArrowAndTabWarning] = useState(false);
const { data: providers } = useProviderList();
const { localCert, isLocalCertMatching, createCertificate, isCreatingCert } = useCertificate();
const providerInfo = providers?.find(p => p.owner === selectedLease?.provider);
Expand Down Expand Up @@ -103,6 +107,12 @@ export const DeploymentLeaseShell: React.FunctionComponent<Props> = ({ leases })
if (message?.data) {
let parsedData = Buffer.from(message.data).toString("utf-8", 1);

// Check if parsedData is either ^[[A, ^[[B, ^[[C or ^[[D
const arrowKeyPattern = /\^\[\[[A-D]/;
if (arrowKeyPattern.test(parsedData)) {
setShowArrowAndTabWarning(true);
}

let exitCode, errorMessage;
try {
const jsonData = JSON.parse(parsedData);
Expand Down Expand Up @@ -268,10 +278,24 @@ export const DeploymentLeaseShell: React.FunctionComponent<Props> = ({ leases })
)}
</Box>

{showArrowAndTabWarning && (
<Alert variant="standard" severity="warning" sx={{ borderRadius: 0, marginBottom: 1 }}>
<Link href={UrlService.faq("shell-arrows-and-completion")} target="_blank" style={{ display: "inline-flex", alignItems: "center" }}>
Why is my UP arrow and TAB autocompletion not working?
<LaunchIcon fontSize={"small"} alignmentBaseline="middle" />
</Link>
</Alert>
)}

<ViewPanel stickToBottom style={{ overflow: "hidden" }}>
{isConnectionClosed && (
<Alert variant="standard" severity="warning" sx={{ borderRadius: 0 }}>
The connection to your Cloudmos Shell was lost.
The connection to your Cloudmos Shell was lost. (
<Link href={UrlService.faq("shell-lost")} target="_blank" style={{ display: "inline-flex", alignItems: "center" }}>
More Info
<LaunchIcon fontSize={"small"} alignmentBaseline="middle" />
</Link>
)
</Alert>
)}
<XTerm ref={terminalRef} onKey={onTerminalKey} onTerminalPaste={onTerminalPaste} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const ShellDownloadModal = ({ selectedLease, onCloseClick, selectedServic
});

const onSubmit = async ({ filePath }) => {
downloadFileFromShell(providerInfo.host_uri, selectedLease.dseq, selectedLease.gseq, selectedLease.oseq, selectedService, filePath);
downloadFileFromShell(providerInfo.hostUri, selectedLease.dseq, selectedLease.gseq, selectedLease.oseq, selectedService, filePath);

event(AnalyticsEvents.DOWNLOADED_SHELL_FILE, {
category: "deployments",
Expand All @@ -68,8 +68,9 @@ export const ShellDownloadModal = ({ selectedLease, onCloseClick, selectedServic
<Dialog open={true} maxWidth="xs" fullWidth onClose={onCloseClick}>
<DialogTitle className={classes.dialogTitle}>Download file</DialogTitle>
<DialogContent>
<Alert severity="info" className={classes.alert}>
<Typography variant="caption">Enter the path of a file on the server to be downloaded. Example: public/index.html</Typography>
<Typography variant="caption">Enter the path of a file on the server to be downloaded to your computer. Example: /app/logs.txt</Typography>
<Alert severity="warning" className={classes.alert}>
<Typography variant="caption">This is an experimental feature and may not work reliably.</Typography>
</Alert>

<form onSubmit={handleSubmit(onSubmit)} ref={formRef}>
Expand All @@ -89,16 +90,20 @@ export const ShellDownloadModal = ({ selectedLease, onCloseClick, selectedServic
control={control}
name="filePath"
rules={{
required: true
required: "File path is required.",
pattern: {
value: /^(?!https?:).*/i,
message: "Should be a valid path on the server, not a URL."
}
}}
render={({ field, fieldState }) => {
return (
<TextField
{...field}
type="text"
label="File path"
error={!!fieldState.invalid}
helperText={fieldState.invalid && "File path is required."}
error={!!fieldState.error}
helperText={fieldState.error?.message}
variant="outlined"
autoFocus
placeholder="Type a valid file path"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import React from "react";
import { makeStyles } from "tss-react/mui";
import { Alert, Box, Button, useTheme } from "@mui/material";
import { Alert, Box, useTheme } from "@mui/material";
import { useRouter } from "next/router";
import Link from "next/link";
import { UrlService } from "@src/utils/urlUtils";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import { ExternalLink } from "../shared/ExternalLink";

const useStyles = makeStyles()(theme => ({
Expand Down
9 changes: 5 additions & 4 deletions deploy-web/src/components/layout/TransactionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ import { Popup } from "../shared/Popup";
import { Box, CircularProgress, Typography } from "@mui/material";

type Props = {
state: "waitingForApproval" | "broadcasting";
open: boolean;
onClose: () => void;
onClose?: () => void;
children?: ReactNode;
};

export const TransactionModal: React.FunctionComponent<Props> = ({ open, onClose }) => {
export const TransactionModal: React.FunctionComponent<Props> = ({ state, open, onClose }) => {
return (
<Popup
fullWidth
open={open}
variant="custom"
title={<>Transaction Pending</>}
title={state === "waitingForApproval" ? <>Waiting for tx approval</> : <>Transaction Pending</>}
actions={[]}
onClose={onClose}
baktun14 marked this conversation as resolved.
Show resolved Hide resolved
maxWidth="xs"
Expand All @@ -26,7 +27,7 @@ export const TransactionModal: React.FunctionComponent<Props> = ({ open, onClose
</Box>

<div>
<Typography variant="caption">BROADCASTING TRANSACTION...</Typography>
<Typography variant="caption">{state === "waitingForApproval" ? "APPROVE OR REJECT TX TO CONTINUE..." : "BROADCASTING TRANSACTION..."}</Typography>
</div>
</Box>
</Popup>
Expand Down
2 changes: 1 addition & 1 deletion deploy-web/src/components/shared/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export const Popup: React.FC<PopupProps> = props => {

if (props.title) {
component.push(
<DialogTitle key="dialog-title" onClose={event => onClose(event, "action")}>
<DialogTitle key="dialog-title" onClose={props.onClose ? event => onClose(event, "action") : undefined}>
{props.title}
</DialogTitle>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,24 +158,29 @@ export const BackgroundTaskProvider = ({ children }) => {
let fileContent: Buffer | null = null;

ws.onmessage = event => {
let jsonData, exitCode, errorMessage;
let exitCode, errorMessage;
try {
const message = JSON.parse(event.data).message;

const bufferData = Buffer.from(message.data.slice(1));
const stringData = bufferData.toString("utf-8").replace(/^\n|\n$/g, "");

jsonData = JSON.parse(stringData);
exitCode = jsonData["exit_code"];
errorMessage = jsonData["message"];
try {
const jsonData = JSON.parse(stringData);
exitCode = jsonData["exit_code"];
errorMessage = jsonData["message"];
} catch (err) {}

if (exitCode !== undefined) {
if (errorMessage) {
console.error(`An error has occured: ${errorMessage}`);
} else if (fileContent === null) {
console.log("File content null");
} else {
console.log("Download done: " + fileContent.length);
isFinished = true;
}
console.log("Download done: " + fileContent.length);

isFinished = true;
ws.close();
} else {
if (!fileContent) {
Expand All @@ -188,6 +193,7 @@ export const BackgroundTaskProvider = ({ children }) => {
}
} catch (error) {
console.log(error);
ws.close();
}
};

Expand All @@ -202,7 +208,7 @@ export const BackgroundTaskProvider = ({ children }) => {
} else {
console.log("No file / Failed");
closeSnackbar(snackbarKey);
enqueueSnackbar("Failed to download logs", { variant: "error" });
enqueueSnackbar("Failed to download file", { variant: "error" });
}
};
ws.onopen = () => {
Expand Down
21 changes: 15 additions & 6 deletions deploy-web/src/context/WalletProvider/WalletProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useSnackbar } from "notistack";
import { Snackbar } from "@src/components/shared/Snackbar";
import { customRegistry } from "@src/utils/customRegistry";
import { TransactionModal } from "@src/components/layout/TransactionModal";
import { OpenInNew, WindowSharp } from "@mui/icons-material";
import { OpenInNew } from "@mui/icons-material";
import { useTheme } from "@mui/material";
import { event } from "nextjs-google-analytics";
import { AnalyticsEvents } from "@src/utils/analytics";
Expand Down Expand Up @@ -69,6 +69,7 @@ export const WalletProvider = ({ children }) => {
const [isWindowLoaded, setIsWindowLoaded] = useState<boolean>(false);
const [isWalletLoaded, setIsWalletLoaded] = useState<boolean>(false);
const [isBroadcastingTx, setIsBroadcastingTx] = useState<boolean>(false);
const [isWaitingForApproval, setIsWaitingForApproval] = useState<boolean>(false);
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const isMounted = useRef(true);
const sigingClient = useRef<SigningStargateClient>(null);
Expand Down Expand Up @@ -264,7 +265,7 @@ export const WalletProvider = ({ children }) => {
}

async function signAndBroadcastTx(msgs: EncodeObject[]): Promise<boolean> {
setIsBroadcastingTx(true);
setIsWaitingForApproval(true);
let pendingSnackbarKey = null;
try {
const client = await getStargateClient();
Expand All @@ -283,7 +284,8 @@ export const WalletProvider = ({ children }) => {
},
""
);

setIsWaitingForApproval(false);
setIsBroadcastingTx(true);
pendingSnackbarKey = enqueueSnackbar(<Snackbar title="Broadcasting transaction..." subTitle="Please wait a few seconds" showLoading />, {
variant: "info",
autoHideDuration: null
Expand All @@ -292,6 +294,8 @@ export const WalletProvider = ({ children }) => {
const txRawBytes = Uint8Array.from(TxRaw.encode(txRaw).finish());
const txResult = await client.broadcastTx(txRawBytes);

setIsBroadcastingTx(false);

if (txResult.code !== 0) {
throw new Error(txResult.rawLog);
}
Expand Down Expand Up @@ -364,11 +368,17 @@ export const WalletProvider = ({ children }) => {
closeSnackbar(pendingSnackbarKey);
}

setIsWaitingForApproval(false);
setIsBroadcastingTx(false);
}
}

const showTransactionSnackbar = (snackTitle, snackMessage, transactionHash, snackVariant) => {
const showTransactionSnackbar = (
snackTitle: string,
snackMessage: string,
transactionHash: string,
snackVariant: React.ComponentProps<typeof Snackbar>["iconVariant"]
) => {
enqueueSnackbar(
<Snackbar
title={snackTitle}
Expand Down Expand Up @@ -426,7 +436,7 @@ export const WalletProvider = ({ children }) => {
>
{children}

<TransactionModal open={isBroadcastingTx} onClose={() => setIsBroadcastingTx(false)} />
<TransactionModal open={isWaitingForApproval || isBroadcastingTx} state={isWaitingForApproval ? "waitingForApproval" : "broadcasting"} />
</WalletProviderContext.Provider>
);
};
Expand Down Expand Up @@ -455,4 +465,3 @@ const TransactionSnackbarContent = ({ snackMessage, transactionHash }) => {
</>
);
};

9 changes: 9 additions & 0 deletions deploy-web/src/pages/faq/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export default function FaqPage() {
<li>
<Link href="#shell-lost">Can't access shell: "The connection to your Cloudmos Shell was lost."</Link>
</li>
<li>
<Link href="#shell-arrows-and-completion">Shell: UP arrow and TAB autocompletion does not work</Link>
</li>
<li>
<Link href="#send-manifest-resources-mismatch">
Error while sending manifest to provider. Error: manifest cross-validation error: group "X": service "X": CPU/Memory resources mismatch for ID 1
Expand Down Expand Up @@ -76,6 +79,12 @@ export default function FaqPage() {
</li>
</ul>

<h2 id="shell-arrows-and-completion">Shell: UP arrow and TAB autocompletion does not work</h2>
<p>
Some docker images use "sh" as the default shell. This shell does not support up arrow and TAB autocompletion. You may try sending the "bash" command
to switch to a bash shell which support those feature.
</p>

<h2 id="send-manifest-resources-mismatch">
Error while sending manifest to provider. Error: manifest cross-validation error: group "X": service "X": CPU/Memory resources mismatch for ID 1
</h2>
Expand Down
2 changes: 1 addition & 1 deletion deploy-web/src/utils/urlUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class UrlService {
static priceCompareCustom = (cpu: number, memory: number, storage: number, memoryUnit: string, storageUnit: string) =>
`/price-compare${appendSearchParams({ cpu, memory, storage, memoryUnit, storageUnit })}`;
static contact = () => "/contact";
static faq = () => "/faq";
static faq = (q?: string) => `/faq${q ? "#" + q : ""}`;
static privacyPolicy = () => "/privacy-policy";
static termsOfService = () => "/terms-of-service";
static blocks = () => `/blocks`;
Expand Down