Skip to content

Commit

Permalink
Improve pending tx modal + shell/download file error handling (#36)
Browse files Browse the repository at this point in the history
* Prevent closing tx modal

* Add link to FAQ in shell connection lost message

* Add faq entry for tab completion

* Show warning when trying to use up arrow in sh shell

* Add warning when trying to download from URL

* Fix download file

* Improve download file error handling

* Use UrlService for faq links
  • Loading branch information
Redm4x authored Oct 19, 2023
1 parent 8eee618 commit 0047efb
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 30 deletions.
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
17 changes: 11 additions & 6 deletions deploy-web/src/components/deploymentDetail/ShellDownloadModal.tsx
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}
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

0 comments on commit 0047efb

Please sign in to comment.