diff --git a/package.json b/package.json
index ed9349c..4fe0d84 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,11 @@
"web"
],
"dependencies": {
- "concurrently": "^7.2.1"
+ "concurrently": "^7.2.1",
+ "path": "^0.12.7"
},
- "version": "0.1.292"
+ "version": "0.1.292",
+ "devDependencies": {
+ "url-loader": "^4.1.1"
+ }
}
diff --git a/web/craco.config.js b/web/craco.config.js
index 461fd75..e0b9a99 100644
--- a/web/craco.config.js
+++ b/web/craco.config.js
@@ -1,12 +1,12 @@
-const webpack = require("webpack");
-const path = require("path");
-const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
+const webpack = require('webpack');
+const path = require('path');
+const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
module.exports = {
webpack: {
plugins: [
new webpack.ProvidePlugin({
- Buffer: ["buffer", "Buffer"],
+ Buffer: ['buffer', 'Buffer'],
}),
new MonacoWebpackPlugin({
languages: ['yaml'],
@@ -20,20 +20,20 @@ module.exports = {
},
},
],
- })
+ }),
],
configure: {
externals: {
- "node:crypto": "crypto",
+ 'node:crypto': 'crypto',
},
resolve: {
alias: {
- perf_hooks: path.resolve(__dirname, "src/perf_hooks.ts"),
- fetch: path.resolve(__dirname, "src/fetch.ts")
+ perf_hooks: path.resolve(__dirname, 'src/perf_hooks.ts'),
+ fetch: path.resolve(__dirname, 'src/fetch.ts'),
},
- extensions: [".tsx", ".ts", ".js"],
+ extensions: ['.tsx', '.ts', '.js', '.jsx', '.json', '.png', '.jpg', '.jpeg', '.gif'],
fallback: {
- buffer: require.resolve("buffer"),
+ buffer: require.resolve('buffer'),
crypto: false,
events: false,
path: false,
@@ -45,9 +45,8 @@ module.exports = {
({ module, details }) => {
// Here we check if warnings are coming from node_modules and are type od source-map
// Than we remove it from console because we don't have any impact on those warnings
- // All other warnings that are coming from App wil yield to dev console
- return module?.resource?.includes("node_modules") &&
- details?.includes("source-map-loader")
+ // All other warnings that are coming from App will yield to the dev console
+ return module?.resource?.includes('node_modules') && details?.includes('source-map-loader');
},
],
},
diff --git a/web/package.json b/web/package.json
index 89960b3..ab4975c 100644
--- a/web/package.json
+++ b/web/package.json
@@ -14,7 +14,7 @@
"@akashnetwork/akashjs": "^0.4.5",
"@cosmjs/launchpad": "^0.27.1",
"@cosmjs/proto-signing": "0.25.4",
- "@craco/craco": "^6.4.5",
+ "@craco/craco": "^6.4.4",
"@emeraldpay/hashicon-react": "^0.5.2",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 909c907..8d18408 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -21,12 +21,13 @@ const MyDeployments = lazy(() => import('./pages/MyDeployments'));
const UpdateDeployment = lazy(() => import('./pages/UpdateDeployment'));
const CustomApp = lazy(() => import('./pages/CustomApp'));
const Provider = lazy(() => import('./pages/Provider'));
+const Landing = lazy(() => import('./pages/Landing'));
const Welcome = () => {
const navigate = useNavigate();
useEffect(() => {
- navigate('/landing/node-deployment');
+ navigate('/landing');
}, []);
return <>>;
@@ -44,8 +45,9 @@ const AppRouter = () => {
+ } />
} />
- }/>
+ } />
} />
} />
@@ -117,11 +119,13 @@ export default function App() {
return (
-
-
-
- }>
+
+
+
+ }
+ >
diff --git a/web/src/Landing-Metadata/landing.ts b/web/src/Landing-Metadata/landing.ts
new file mode 100644
index 0000000..bfcd52b
--- /dev/null
+++ b/web/src/Landing-Metadata/landing.ts
@@ -0,0 +1,109 @@
+import img1 from './landingIcons/first_img.png';
+import img2 from './landingIcons/www.png';
+import img3 from './landingIcons/chip.png';
+import img33 from './landingIcons/code.png';
+import img4 from './landingIcons/last_guide.png';
+import img5 from './landingIcons/sdl_2.png';
+import img6 from './landingIcons/sdl_22.png';
+
+interface Tile {
+ title: string;
+ description: string;
+ image: string;
+ buttonText: string;
+ route: string;
+ icon: string;
+ buttonEnabled: boolean;
+ buttonClass?: string;
+}
+
+interface CategoryTiles {
+ introText: string;
+ tiles: Tile[];
+}
+
+interface Metadata {
+ version: string;
+ categoriesTiles: CategoryTiles;
+ sdlGuideTiles: {
+ introText: string;
+ introDescription: string;
+ tiles: {
+ step: string;
+ text: string;
+ image: string;
+ }[];
+ };
+}
+
+export const metadata: Metadata = {
+ version: '0.0.1',
+ categoriesTiles: {
+ introText: 'What would you like to do today?',
+ tiles: [
+ {
+ title: 'Deploy a Blockchain Node',
+ description:
+ 'Easy and low cost hosting for your blockchain nodes (RPC servers, Validators and more)',
+ buttonText: 'Choose a Template',
+ route: '/landing/node-deployment',
+ icon: 'xrayView',
+ image: img1,
+ buttonEnabled: true,
+ },
+ {
+ title: 'Host a Website or Web Service',
+ description:
+ 'Low cost, decentralized equivalents of the services provided by mainstream cloud providers. Host websites, blogsites, databases and more.',
+ buttonText: 'Coming Soon',
+ route: '',
+ icon: 'www',
+ image: img2,
+ buttonEnabled: true,
+ buttonClass: 'coming-soon-btn-2',
+ },
+ {
+ title: 'Deploy an AI/ ML Model',
+ description:
+ 'Popular AI & ML models, deployed in just a few clicks. Includes Stable Diffusion, GPT4All, Alpaca and more.',
+ buttonText: 'Coming Soon',
+ route: '',
+ icon: 'electronicsChip',
+ image: img3,
+ buttonEnabled: true,
+ },
+ {
+ title: 'Custom Application',
+ description:
+ 'Define your unique deployment requirements and preferences with SDL and deploy with ease on the flexible and reliable Akash network.',
+ buttonText: 'Import SDL',
+ route: '',
+ icon: 'electronicsChip',
+ image: img33,
+ buttonEnabled: true,
+ },
+ ],
+ },
+ sdlGuideTiles: {
+ introText: 'How it works?',
+ introDescription:
+ 'There are 3 main steps to deploying on Akash. Check out our detailed help for more.',
+ tiles: [
+ {
+ step: '01',
+ text: 'Start with a template or your own custom application (SDL)',
+ image: img6,
+ },
+ {
+ step: '02',
+ text: 'Choose a provider based on your preferences and desired price',
+ image: img5,
+ },
+ {
+ step: '03',
+ text: 'View & manage your deployed application',
+ image: img4,
+ },
+ ],
+ },
+};
diff --git a/web/src/Landing-Metadata/landingIcons/chip.png b/web/src/Landing-Metadata/landingIcons/chip.png
new file mode 100644
index 0000000..8286a7e
Binary files /dev/null and b/web/src/Landing-Metadata/landingIcons/chip.png differ
diff --git a/web/src/Landing-Metadata/landingIcons/code.png b/web/src/Landing-Metadata/landingIcons/code.png
new file mode 100644
index 0000000..0db22e5
Binary files /dev/null and b/web/src/Landing-Metadata/landingIcons/code.png differ
diff --git a/web/src/Landing-Metadata/landingIcons/first_img.png b/web/src/Landing-Metadata/landingIcons/first_img.png
new file mode 100644
index 0000000..1783629
Binary files /dev/null and b/web/src/Landing-Metadata/landingIcons/first_img.png differ
diff --git a/web/src/Landing-Metadata/landingIcons/last_guide.png b/web/src/Landing-Metadata/landingIcons/last_guide.png
new file mode 100644
index 0000000..576dcdb
Binary files /dev/null and b/web/src/Landing-Metadata/landingIcons/last_guide.png differ
diff --git a/web/src/Landing-Metadata/landingIcons/sdl_2.png b/web/src/Landing-Metadata/landingIcons/sdl_2.png
new file mode 100644
index 0000000..6ca6df1
Binary files /dev/null and b/web/src/Landing-Metadata/landingIcons/sdl_2.png differ
diff --git a/web/src/Landing-Metadata/landingIcons/sdl_22.png b/web/src/Landing-Metadata/landingIcons/sdl_22.png
new file mode 100644
index 0000000..5ae1855
Binary files /dev/null and b/web/src/Landing-Metadata/landingIcons/sdl_22.png differ
diff --git a/web/src/Landing-Metadata/landingIcons/www.png b/web/src/Landing-Metadata/landingIcons/www.png
new file mode 100644
index 0000000..68989be
Binary files /dev/null and b/web/src/Landing-Metadata/landingIcons/www.png differ
diff --git a/web/src/components/Deployment/index.tsx b/web/src/components/Deployment/index.tsx
index 2bf1dd5..fc38374 100644
--- a/web/src/components/Deployment/index.tsx
+++ b/web/src/components/Deployment/index.tsx
@@ -24,7 +24,7 @@ import { getRpcNode } from '../../hooks/useRpcNode';
import { QueryDeploymentResponse as Beta3Deployment } from '@akashnetwork/akashjs/build/protobuf/akash/deployment/v1beta3/query';
import { QueryDeploymentResponse as Beta2Deployment } from '@akashnetwork/akashjs/build/protobuf/akash/deployment/v1beta2/query';
-import DeploymentActionButton from './DeploymentActionButton';
+// import DeploymentActionButton from './DeploymentActionButton';
const Deployment: React.FC = () => {
const { dseq } = useParams();
@@ -271,8 +271,8 @@ const Deployment: React.FC = () => {
)}
{deployment?.deployment && !deploymentIncomplete && (
- = () => {
aria-controls="menu-appbar"
aria-haspopup="true"
startIcon={}
- >
+
Update Deployment
-
+
- = () => {
aria-controls="menu-appbar"
aria-haspopup="true"
startIcon={}
- >
Update Deployment
-
+
= () => {
color="secondary"
aria-label="re-deploy"
sx={{
- justifyContent: 'left'
+ justifyContent: 'left',
}}
startIcon={}
>
diff --git a/web/src/components/DeploymentStepper/index.tsx b/web/src/components/DeploymentStepper/index.tsx
index 7444dc5..62d7559 100644
--- a/web/src/components/DeploymentStepper/index.tsx
+++ b/web/src/components/DeploymentStepper/index.tsx
@@ -55,7 +55,8 @@ const DeploymentStepper: React.FC = () => {
const [errorMessage, setErrorMessage] = React.useState();
const [myDeployments, setMyDeployments] = useRecoilState(myDeploymentsAtom);
const [, setDeploymentRefresh] = useRecoilState(deploymentDataStale);
- const { mutate: mxCreateDeployment, isLoading: deploymentProgressVisible } = useMutation(createDeployment);
+ const { mutate: mxCreateDeployment, isLoading: deploymentProgressVisible } =
+ useMutation(createDeployment);
const { mutate: mxCreateLease, isLoading: leaseProgressVisible } = useMutation(createLease);
const { mutate: mxSendManifest, isLoading: manifestSending } = useMutation(sendManifest);
@@ -115,26 +116,29 @@ const DeploymentStepper: React.FC = () => {
const _sdl = sdl ? sdl : cachedDetails.sdl;
if (lease) {
- mxSendManifest({ address: keplr.accounts[0].address, lease, sdl: _sdl}, {
- onSuccess: (result) => {
+ mxSendManifest(
+ { address: keplr.accounts[0].address, lease, sdl: _sdl },
+ {
+ onSuccess: (result) => {
if (result) {
logging.success('Manifest sending: successful');
setDeploymentRefresh(true);
navigate(`/my-deployments/${dseq}`);
}
},
- onError: (error) => {
- logging.log(`Failed to send manifest: ${error}`);
- },
- onSettled: () => {
- navigate(`/my-deployments/${dseq}`);
+ onError: (error) => {
+ logging.log(`Failed to send manifest: ${error}`);
+ },
+ onSettled: () => {
+ navigate(`/my-deployments/${dseq}`);
+ },
}
- });
+ );
}
},
onError: (error) => {
logging.log(`Failed to create lease: ${error}`);
- }
+ },
});
};
@@ -185,7 +189,8 @@ const DeploymentStepper: React.FC = () => {
setCardMessage('Creating deployment');
try {
- const result = mxCreateDeployment({ sdl: value.sdl, depositor: value.depositor },
+ const result = mxCreateDeployment(
+ { sdl: value.sdl, depositor: value.depositor },
{
onSuccess: async (result) => {
if (result && result.deploymentId) {
@@ -202,7 +207,7 @@ const DeploymentStepper: React.FC = () => {
// head to the bid selection page
navigate(`/configure-deployment/${result.deploymentId.dseq}`);
}
- }
+ },
}
);
} catch (error) {
@@ -251,43 +256,48 @@ const DeploymentStepper: React.FC = () => {
{activeStep.currentCard === steps.length
? null
: !progressVisible && (
-
- {activeStep.currentCard === 0 && (
- {
- selectFolder(folderName);
- }}
- callback={(sdl) =>
- navigate('/new-deployment/custom-sdl', { state: { sdl: sdl } })
- }
- setFieldValue={setFieldValue}
- />
- )}
- {activeStep.currentCard === 1 && folderName && (
-
- )}
- {activeStep.currentCard === 2 && folderName && templateId && (
- handlePreflightCheck(intent, values.sdl)}
- />
- )}
- {activeStep.currentCard === 3 && }
- {activeStep.currentCard === 4 && deploymentId && (
- }>
- acceptBid(bidId)}
+
+ {activeStep.currentCard === 0 && (
+ {
+ selectFolder(folderName);
+ }}
+ callback={(sdl) =>
+ navigate('/new-deployment/custom-sdl', { state: { sdl: sdl } })
+ }
+ setFieldValue={setFieldValue}
+ onSave={function (sdl: any): void {
+ throw new Error('Function not implemented.');
+ }}
+ />
+ )}
+ {activeStep.currentCard === 1 && folderName && (
+
+ )}
+ {activeStep.currentCard === 2 && folderName && templateId && (
+
+ handlePreflightCheck(intent, values.sdl)
+ }
/>
-
- )}
-
- )}
+ )}
+ {activeStep.currentCard === 3 && }
+ {activeStep.currentCard === 4 && deploymentId && (
+ }>
+ acceptBid(bidId)}
+ />
+
+ )}
+
+ )}
>
);
}}
diff --git a/web/src/components/MonacoYamlEditor/index.tsx b/web/src/components/MonacoYamlEditor/index.tsx
index 238a24c..e61d3b5 100644
--- a/web/src/components/MonacoYamlEditor/index.tsx
+++ b/web/src/components/MonacoYamlEditor/index.tsx
@@ -18,6 +18,7 @@ interface MonacoYamlEditorProps {
closeReviewModal: () => void;
onSaveButtonClick: (value: SDLSpec) => void;
disabled: boolean;
+ onSave: (sdl: any) => void; // Add onSave prop
}
export const MonacoYamlEditor: React.FC = ({
diff --git a/web/src/components/SdlConfiguration/SdlConfiguration.tsx b/web/src/components/SdlConfiguration/SdlConfiguration.tsx
index 1ae7add..6ae67cd 100644
--- a/web/src/components/SdlConfiguration/SdlConfiguration.tsx
+++ b/web/src/components/SdlConfiguration/SdlConfiguration.tsx
@@ -37,6 +37,7 @@ interface SdlConfigurationProps {
configurationType: SdlConfigurationType;
progressVisible?: boolean;
cardMessage?: string | undefined;
+ onSave: (sdl: any) => void;
}
export const SdlConfiguration: React.FC = ({
@@ -47,6 +48,7 @@ export const SdlConfiguration: React.FC = ({
configurationType,
progressVisible,
cardMessage,
+ onSave,
}) => {
const forbidEditing = configurationType === SdlConfigurationType.Update;
@@ -200,6 +202,7 @@ export const SdlConfiguration: React.FC = ({
reviewSdl={reviewSdl}
closeReviewModal={closeReviewModal}
disabled={forbidEditing}
+ onSave={onSave} // Pass the onSave prop
/>
diff --git a/web/src/components/SdlConfiguration/SdllEditor.tsx b/web/src/components/SdlConfiguration/SdllEditor.tsx
index a7f8bef..6178a35 100644
--- a/web/src/components/SdlConfiguration/SdllEditor.tsx
+++ b/web/src/components/SdlConfiguration/SdllEditor.tsx
@@ -9,6 +9,7 @@ interface SdlEditorProps {
closeReviewModal: () => void;
disabled?: boolean;
callback?: (sdl: any) => void;
+ onSave: (sdl: any) => void;
}
export const SdlEditor: React.FC = ({
@@ -16,6 +17,7 @@ export const SdlEditor: React.FC = ({
closeReviewModal,
disabled,
callback,
+ onSave, // Add the onSave prop
}) => {
return (
@@ -35,8 +37,13 @@ export const SdlEditor: React.FC = ({
if (callback) {
return callback(value);
}
+
+ if (onSave) {
+ return onSave(value);
+ }
form.setFieldValue(field.name, value);
}}
+ onSave={onSave}
/>
);
}}
diff --git a/web/src/components/TileCard/TileCard.tsx b/web/src/components/TileCard/TileCard.tsx
new file mode 100644
index 0000000..2e6d376
--- /dev/null
+++ b/web/src/components/TileCard/TileCard.tsx
@@ -0,0 +1,233 @@
+/* eslint-disable quotes */
+import React, { Suspense, useState, useCallback } from 'react';
+import { useRecoilState, useRecoilValue } from 'recoil';
+import { SdlEditor } from '../../components/SdlConfiguration/SdllEditor';
+import { Link, useNavigate, useParams } from 'react-router-dom';
+import { Box, Button, Card, CardActions, CardContent, Typography } from '@mui/material';
+import styled from '@emotion/styled';
+import { Formik } from 'formik';
+import {
+ deploymentDataStale,
+ deploymentSdl,
+ keplrState,
+ myDeployments as myDeploymentsAtom,
+} from '../../recoil/atoms';
+interface Props {
+ item: {
+ route: string;
+ title: string;
+ image: string;
+ description: string;
+ buttonText: string;
+ buttonClass?: string;
+ };
+}
+
+import { initialValues, InitialValuesProps, SDLSpec } from '../SdlConfiguration/settings';
+import { myDeploymentFormat } from '../../_helpers/my-deployment-utils';
+import { Deployment } from '@akashnetwork/akashjs/build/protobuf/akash/deployment/v1beta2/deployment';
+import { useMutation } from 'react-query';
+import { createDeployment, createLease, sendManifest } from '../../api/mutations';
+
+const steps = ['Featured Apps', 'Select', 'Configure', 'Review', 'Deploy'];
+
+export interface DeploymentStepperProps {
+ dseq?: string;
+ leaseId?: string;
+}
+
+function TileCard(props: Props) {
+ const { title, image, description, buttonText, buttonClass } = props.item;
+ const keplr = useRecoilValue(keplrState);
+ const navigate = useNavigate();
+ const [deploymentId, setDeploymentId] = React.useState<{ owner: string; dseq: string }>();
+ const { dseq } = useParams();
+ const [sdl, setSdl] = useRecoilState(deploymentSdl);
+ const [cardMessage, setCardMessage] = useState('');
+ const [activeStep, setActiveStep] = useState({ currentCard: 0 });
+
+ const [open, setOpen] = React.useState(false);
+ const [errorTitle, setErrorTitle] = React.useState();
+ const [errorMessage, setErrorMessage] = React.useState();
+ const [myDeployments, setMyDeployments] = useRecoilState(myDeploymentsAtom);
+ const [setDeploymentRefresh] = useRecoilState(deploymentDataStale);
+ const { mutate: mxCreateDeployment, isLoading: deploymentProgressVisible } =
+ useMutation(createDeployment);
+
+ const bull = (
+
+ •
+
+ );
+
+ React.useEffect(() => {
+ if (dseq) {
+ setDeploymentId({
+ owner: keplr.accounts[0].address,
+ dseq,
+ });
+ setActiveStep({ currentCard: 4 });
+ return;
+ }
+ }, [dseq, keplr]);
+
+ const handleSdlEditorSave = (sdl: any) => {
+ navigate('/new-deployment/custom-sdl', { state: { sdl: sdl } });
+ };
+
+ const handleDeployment = (key: string, deployment: any) => {
+ const newDeployments: { [key: string]: Deployment } = { ...myDeployments };
+ newDeployments[key] = deployment;
+ setMyDeployments(newDeployments);
+ };
+
+ const [reviewSdl, setReviewSdl] = useState(false);
+ const closeReviewModal = useCallback(() => setReviewSdl(false), []);
+
+ const handleImportSDL = () => {
+ setReviewSdl(true);
+ };
+
+ // TODO: this should be changed to use the logging system, and not throw
+ // additional exceptions.
+ const handleError = async (maybeError: unknown, method: string) => {
+ const error =
+ maybeError && Object.prototype.hasOwnProperty.call(maybeError, 'message')
+ ? (maybeError as Error)
+ : { message: 'Unknown error' };
+
+ let title = 'Error';
+ let message = 'An error occurred while sending your request.';
+ if (method === 'acceptBid') {
+ title = 'Error Select Provider';
+ message = 'An error occurred while selecting a provider.';
+ }
+ if (method === 'createDeployment') {
+ title = 'Error Create Deployment';
+ message = 'An error occurred while trying to deploy.';
+ if (error.message.includes('Query failed with (6)')) {
+ message = 'There was an RPC error. This may happen during upgrades to the Akash Network.';
+ }
+ }
+ setErrorTitle(title);
+ setErrorMessage(message);
+ setCardMessage('');
+ setOpen(true);
+ throw new Error(`${method}: ${error.message}`);
+ };
+
+ return (
+ {
+ // the onSubmit method is called from the component PreflightCheck.
+ setCardMessage('Creating deployment');
+
+ try {
+ const result = mxCreateDeployment(
+ { sdl: value.sdl, depositor: value.depositor },
+ {
+ onSuccess: async (result) => {
+ if (result && result.deploymentId) {
+ setDeploymentId(result.deploymentId);
+ setSdl(value.sdl);
+
+ // set deployment to localStorage object using Atom
+ const _deployment = await myDeploymentFormat(result, value);
+ handleDeployment(_deployment.key, JSON.stringify(_deployment.data));
+
+ // set deployment to localStorage item by dseq (deprecate ?)
+ localStorage.setItem(_deployment.key, JSON.stringify(_deployment.data));
+
+ // head to the bid selection page
+ navigate(`/configure-deployment/${result.deploymentId.dseq}`);
+ }
+ },
+ }
+ );
+ } catch (error) {
+ await handleError(error, 'createDeployment');
+ }
+ }}
+ >
+ {({ setFieldValue, values }) => (
+ <>
+
+
+
+
+
+
{title}
+
+
+
{description}
+ {buttonText === 'Import SDL' && (
+
+
+
+
+
+ )}
+
+ {buttonText === 'Choose a Template' && (
+
+
+
+
+
+ )}
+
+ {buttonText === 'Coming Soon' && (
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ >
+ )}
+
+ );
+}
+
+export default TileCard;
+
+const TemplateBtn = styled.div`
+ display: flex;
+ justify-content: center;
+ margin-top: 40px;
+ Button {
+ width: 100%;
+ padding: 8px, 16px, 8px, 16px;
+ }
+`;
diff --git a/web/src/pages/ConfigureApp.tsx b/web/src/pages/ConfigureApp.tsx
index 1dd91c2..85a774f 100644
--- a/web/src/pages/ConfigureApp.tsx
+++ b/web/src/pages/ConfigureApp.tsx
@@ -37,6 +37,11 @@ export const ConfigureApp: React.FC = ({
keepPreviousData: true,
});
+ const handleSave = (sdl: any) => {
+ // Update the form values with the saved SDL
+ form.setFieldValue('sdl', sdl);
+ };
+
useEffect(() => {
// don't override the value if it's already set
if (form.values?.sdl?.version) return;
@@ -70,6 +75,7 @@ export const ConfigureApp: React.FC = ({
configurationType={SdlConfigurationType.Create}
progressVisible={progressVisible}
cardMessage={cardMessage}
+ onSave={handleSave} // Add the onSave prop
actionItems={() => (