This is an automated email from the ASF dual-hosted git repository.
apratim pushed a commit to branch main
in repository
https://gitbox.apache.org/repos/asf/incubator-resilientdb-resvault.git
The following commit(s) were added to refs/heads/main by this push:
new 2ab75da Added key upload functionality
2ab75da is described below
commit 2ab75da76e3b0df3a72e3aa443614c1fa3dd2ad0
Author: Apratim Shukla <[email protected]>
AuthorDate: Sat Oct 12 12:45:00 2024 -0700
Added key upload functionality
- Exported key pairs can now be imported into your ResVault profile.
- Added verification checks for uploaded key pairs.
- Added redundancy checks for uploaded key pairs.
- UI modifications corresponding to these changes.
---
src/context/GlobalContext.js | 141 ++++++++++++++++++++++++++++++++--
src/css/App.css | 74 ++++++++++++++++--
src/pages/Dashboard.jsx | 177 +++++++++++++++++++++++++++++++++----------
3 files changed, 337 insertions(+), 55 deletions(-)
diff --git a/src/context/GlobalContext.js b/src/context/GlobalContext.js
index 5615c56..98512cd 100644
--- a/src/context/GlobalContext.js
+++ b/src/context/GlobalContext.js
@@ -16,6 +16,9 @@ export const GlobalProvider = ({ children }) => {
const [selectedKeyPairIndex, setSelectedKeyPairIndex] = useState(0);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [storedPassword, setStoredPassword] = useState('');
+
+ // Alert state for modals
+ const [alert, setAlert] = useState({ isOpen: false, message: '' });
// Function to encrypt and store key pairs
const saveKeyPairsToStorage = (keyPairs, password) => {
@@ -39,6 +42,7 @@ export const GlobalProvider = ({ children }) => {
callback(decryptedKeyPairs);
} catch (err) {
console.error('Error decrypting key pairs:', err);
+ setAlert({ isOpen: true, message: 'Failed to decrypt key pairs.
Please check your password.' });
callback([]);
}
} else {
@@ -47,6 +51,92 @@ export const GlobalProvider = ({ children }) => {
});
};
+ // Function to append new key pairs while preventing duplicates and
validating them
+ const appendKeyPairs = (newKeyPairs) => {
+ const password = storedPassword;
+ if (!password) {
+ console.error('Password is not available');
+ setAlert({ isOpen: true, message: 'Password is not available. Please
log in again.' });
+ return;
+ }
+
+ // Validate each key pair
+ for (let i = 0; i < newKeyPairs.length; i++) {
+ const keyPair = newKeyPairs[i];
+ if (!keyPair.publicKey || !keyPair.privateKey) {
+ setAlert({ isOpen: true, message: `Key pair at index ${i} is
missing publicKey or privateKey.` });
+ console.error(`Key pair at index ${i} is missing publicKey or
privateKey.`);
+ return;
+ }
+
+ try {
+ // Decode private key from Base58
+ const decodedPrivateKey = Base58.decode(keyPair.privateKey);
+ if (decodedPrivateKey.length !== 32) {
+ setAlert({ isOpen: true, message: `Private key at index ${i}
is not 32 bytes.` });
+ console.error(`Private key at index ${i} is not 32 bytes.`);
+ return;
+ }
+
+ // Derive public key from private key using
nacl.sign.keyPair.fromSeed
+ const derivedKeyPair =
nacl.sign.keyPair.fromSeed(decodedPrivateKey);
+ const derivedPublicKey = Base58.encode(derivedKeyPair.publicKey);
+
+ // Compare derived public key with provided public key
+ if (derivedPublicKey !== keyPair.publicKey) {
+ setAlert({ isOpen: true, message: `Public key does not match
private key at index ${i}.` });
+ console.error(`Public key does not match private key at index
${i}.`);
+ return;
+ }
+ } catch (err) {
+ console.error('Error validating key pair:', err);
+ setAlert({ isOpen: true, message: `Error validating key pair at
index ${i}.` });
+ return;
+ }
+ }
+
+ // Load existing key pairs
+ loadKeyPairsFromStorage(password, (existingKeyPairs) => {
+ // Filter out duplicates
+ const uniqueNewKeyPairs = newKeyPairs.filter(newKey => {
+ return !existingKeyPairs.some(existingKey =>
+ existingKey.publicKey === newKey.publicKey &&
+ existingKey.privateKey === newKey.privateKey
+ );
+ });
+
+ if (uniqueNewKeyPairs.length === 0) {
+ console.log('No new unique key pairs to add.');
+ setAlert({ isOpen: true, message: 'No new unique key pairs to
add.' });
+ return;
+ }
+
+ const updatedKeyPairs = [...existingKeyPairs, ...uniqueNewKeyPairs];
+ saveKeyPairsToStorage(updatedKeyPairs, password);
+ setKeyPairs(updatedKeyPairs);
+
+ // Update selected key pair to the last one added
+ const newIndex = updatedKeyPairs.length - 1;
+ setSelectedKeyPairIndex(newIndex);
+ setPublicKey(updatedKeyPairs[newIndex].publicKey);
+ setPrivateKey(updatedKeyPairs[newIndex].privateKey);
+ saveSelectedKeyPairIndex(newIndex);
+
+ // Update 'store' with the new key pair
+ const encryptedPrivateKey =
CryptoJS.AES.encrypt(updatedKeyPairs[newIndex].privateKey, password).toString();
+ const hash = CryptoJS.SHA256(password).toString(CryptoJS.enc.Hex);
+ const store = {
+ hash,
+ publicKey: updatedKeyPairs[newIndex].publicKey,
+ encryptedPrivateKey: encryptedPrivateKey,
+ history: [],
+ };
+ chrome.storage.sync.set({ store }, () => {
+ console.log('Store updated with new key pair');
+ });
+ });
+ };
+
// Function to save selected key pair index
const saveSelectedKeyPairIndex = (index) => {
chrome.storage.local.set({ selectedKeyPairIndex: index }, () => {});
@@ -65,16 +155,29 @@ export const GlobalProvider = ({ children }) => {
const password = storedPassword;
if (!password) {
console.error('Password is not available');
+ setAlert({ isOpen: true, message: 'Password is not available. Please log
in again.' });
return;
}
const keyPair = nacl.sign.keyPair();
const newPublicKey = Base58.encode(keyPair.publicKey);
- const newPrivateKey = Base58.encode(keyPair.secretKey.slice(0, 32));
+ const newPrivateKey = Base58.encode(keyPair.secretKey.slice(0, 32)); //
Using the first 32 bytes as seed
const newKeyPair = { publicKey: newPublicKey, privateKey: newPrivateKey };
// Load existing key pairs
loadKeyPairsFromStorage(password, (existingKeyPairs) => {
+ // Check for duplicates before adding
+ const isDuplicate = existingKeyPairs.some(existingKey =>
+ existingKey.publicKey === newKeyPair.publicKey &&
+ existingKey.privateKey === newKeyPair.privateKey
+ );
+
+ if (isDuplicate) {
+ console.log('Generated key pair is a duplicate. Skipping.');
+ setAlert({ isOpen: true, message: 'Generated key pair is a
duplicate. Skipping.' });
+ return;
+ }
+
const updatedKeyPairs = [...existingKeyPairs, newKeyPair];
// Save updated key pairs
saveKeyPairsToStorage(updatedKeyPairs, password);
@@ -109,12 +212,14 @@ export const GlobalProvider = ({ children }) => {
const password = storedPassword;
if (!password) {
console.error('Password is not available');
+ setAlert({ isOpen: true, message: 'Password is not available. Please
log in again.' });
return;
}
loadKeyPairsFromStorage(password, (existingKeyPairs) => {
if (existingKeyPairs.length <= 1) {
console.error('Cannot delete the last remaining key pair.');
+ setAlert({ isOpen: true, message: 'Cannot delete the last
remaining key pair.' });
return;
}
@@ -122,16 +227,32 @@ export const GlobalProvider = ({ children }) => {
const updatedKeyPairs = [...existingKeyPairs];
updatedKeyPairs.splice(index, 1);
- // Immediately update the key pairs state
- setKeyPairs(updatedKeyPairs);
-
// Save the updated keyPairs back to storage
saveKeyPairsToStorage(updatedKeyPairs, password);
+ setKeyPairs(updatedKeyPairs);
// Reset to the first key pair after deletion
setSelectedKeyPairIndex(0);
- setPublicKey(updatedKeyPairs.length > 0 ? updatedKeyPairs[0].publicKey
: '');
- setPrivateKey(updatedKeyPairs.length > 0 ?
updatedKeyPairs[0].privateKey : '');
+ if (updatedKeyPairs.length > 0) {
+ setPublicKey(updatedKeyPairs[0].publicKey);
+ setPrivateKey(updatedKeyPairs[0].privateKey);
+ saveSelectedKeyPairIndex(0);
+ }
+
+ // Update 'store' with the new selected key pair
+ if (updatedKeyPairs.length > 0) {
+ const encryptedPrivateKey =
CryptoJS.AES.encrypt(updatedKeyPairs[0].privateKey, password).toString();
+ const hash = CryptoJS.SHA256(password).toString(CryptoJS.enc.Hex);
+ const store = {
+ hash,
+ publicKey: updatedKeyPairs[0].publicKey,
+ encryptedPrivateKey: encryptedPrivateKey,
+ history: [],
+ };
+ chrome.storage.sync.set({ store }, () => {
+ console.log('Store updated with selected key pair after
deletion');
+ });
+ }
// Optionally call the callback
if (callback) {
@@ -172,7 +293,7 @@ export const GlobalProvider = ({ children }) => {
}, []);
// Function to set selected key pair
- const setSelectedKeyPair = (index) => {
+ const setSelectedKeyPairFn = (index) => {
if (keyPairs[index]) {
setPublicKey(keyPairs[index].publicKey);
setPrivateKey(keyPairs[index].privateKey);
@@ -182,6 +303,7 @@ export const GlobalProvider = ({ children }) => {
const password = storedPassword;
if (!password) {
console.error('Password is not available');
+ setAlert({ isOpen: true, message: 'Password is not available. Please
log in again.' });
return;
}
const encryptedPrivateKey =
CryptoJS.AES.encrypt(keyPairs[index].privateKey, password).toString();
@@ -216,12 +338,15 @@ export const GlobalProvider = ({ children }) => {
generateKeyPair,
selectedKeyPairIndex,
setSelectedKeyPairIndex,
- setSelectedKeyPair,
+ setSelectedKeyPair: setSelectedKeyPairFn,
isAuthenticated,
setIsAuthenticated,
storedPassword,
setStoredPassword,
deleteKeyPair,
+ appendKeyPairs,
+ alert, // For alert modal
+ setAlert, // For alert modal
}}
>
{children}
diff --git a/src/css/App.css b/src/css/App.css
index d95dd29..fd9fcd1 100644
--- a/src/css/App.css
+++ b/src/css/App.css
@@ -3706,33 +3706,95 @@ tr:hover {
opacity: 0.7;
}
+/* Container for keypair actions */
.keypair-actions {
display: flex;
- justify-content: space-between;
+ gap: 10px;
+ justify-content: center;
margin-top: 10px;
}
+/* Badge Button Styles */
.badge-button {
background-color: #3c4e63; /* Matches your theme's primary color */
color: white;
border: none;
- border-radius: 20px;
- padding: 10px 20px;
+ border-radius: 50%; /* Makes the button circular */
+ padding: 10px; /* Uniform padding */
font-size: 14px;
cursor: pointer;
- width: 48%; /* Ensures equal width for both buttons */
- text-align: center;
+ width: 40px; /* Fixed width suitable for icons */
+ height: 40px; /* Fixed height suitable for icons */
display: flex;
align-items: center;
justify-content: center;
+ transition: background-color 0.3s;
}
.badge-button:hover {
background-color: #50647e; /* A slightly lighter shade for hover effect */
}
+/* Remove margin-left from SVGs since there's no text */
.badge-button svg {
- margin-left: 8px; /* Adds space between the text and icon */
+ margin: 0; /* Eliminates any unintended spacing */
+ width: 24px; /* Adjust icon size as needed */
+ height: 24px; /* Adjust icon size as needed */
}
+/* Tooltip container */
+.button-with-tooltip {
+ position: relative;
+ display: inline-block;
+}
+
+/* Tooltip text */
+.tooltip-text {
+ visibility: hidden;
+ width: max-content;
+ background-color: black;
+ color: #fff;
+ text-align: center;
+ border-radius: 6px;
+ padding: 5px 8px;
+ position: absolute;
+ z-index: 1;
+ bottom: 125%; /* Position above the button */
+ left: 50%;
+ transform: translateX(-50%);
+ opacity: 0;
+ transition: opacity 0.3s;
+}
+/* Tooltip arrow */
+.tooltip-text::after {
+ content: "";
+ position: absolute;
+ top: 100%; /* At the bottom of the tooltip */
+ left: 50%;
+ margin-left: -5px;
+ border-width: 5px;
+ border-style: solid;
+ border-color: black transparent transparent transparent;
+}
+
+/* Show the tooltip text when hovering over the container */
+.button-with-tooltip:hover .tooltip-text {
+ visibility: visible;
+ opacity: 1;
+}
+
+/* Additional styling for .keypair-actions to align buttons */
+.keypair-actions {
+ display: flex;
+ gap: 10px;
+ justify-content: center;
+ margin-top: 10px;
+}
+
+/* Centered icon class (optional if not needed) */
+.centered-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
\ No newline at end of file
diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
index df05436..bb28209 100644
--- a/src/pages/Dashboard.jsx
+++ b/src/pages/Dashboard.jsx
@@ -25,6 +25,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import DownloadIcon from '@mui/icons-material/Download';
import DeleteIcon from '@mui/icons-material/Delete';
import SaveAltIcon from '@mui/icons-material/SaveAlt';
+import UploadIcon from '@mui/icons-material/Upload';
import React, { useRef, useState, useEffect, useContext } from 'react';
import Lottie from 'react-lottie';
import versionData from '../data/version.json';
@@ -47,6 +48,9 @@ function Dashboard() {
setSelectedKeyPair,
setIsAuthenticated,
deleteKeyPair,
+ appendKeyPairs,
+ alert, // For alert modal
+ setAlert, // For alert modal
} = useContext(GlobalContext);
const [tabId, setTabId] = useState(null);
@@ -64,16 +68,17 @@ function Dashboard() {
const [error, setError] = useState('');
const [jsonFileName, setJsonFileName] = useState('');
const fileInputRef = useRef(null);
+ const keyPairFileInputRef = useRef(null);
const navigate = useNavigate();
- // New state variables for transaction data and handling
+ // State variables for transaction data and handling
const [transactionData, setTransactionData] = useState(null);
const [transactionError, setTransactionError] = useState('');
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [successResponse, setSuccessResponse] = useState(null);
const [showDeleteModal, setShowDeleteModal] = useState(false);
- // New state for copying transaction ID
+ // State for copying transaction ID
const [isIdCopied, setIsIdCopied] = useState(false);
const defaultOptions = {
@@ -225,6 +230,7 @@ function Dashboard() {
const addNet = () => {
if (!newNetName.trim() || !customUrl.trim()) {
setError('Both fields are required.');
+ setAlert({ isOpen: true, message: 'Both Net Name and GraphQL URL are
required.' });
return;
}
setError('');
@@ -245,6 +251,7 @@ function Dashboard() {
const toggleConnection = () => {
if (!publicKey || !privateKey) {
+ setAlert({ isOpen: true, message: 'Public or Private key is missing.'
});
console.error('Public or Private key is missing');
return;
}
@@ -363,7 +370,7 @@ function Dashboard() {
setSelectedKeyPairIndex(0);
setSelectedKeyPair(0);
});
-
+
// Close the delete confirmation modal
setShowDeleteModal(false);
}
@@ -383,6 +390,7 @@ function Dashboard() {
setIsCopied(false);
}, 1500);
} catch (err) {
+ setAlert({ isOpen: true, message: 'Unable to copy text.' });
console.error('Unable to copy text: ', err);
}
};
@@ -433,11 +441,13 @@ function Dashboard() {
} else {
setTransactionData(null);
setTransactionError('Invalid JSON format: Missing required
fields.');
+ setAlert({ isOpen: true, message: 'Invalid JSON format:
Missing required fields.' });
}
} catch (err) {
console.error('Error parsing JSON:', err);
setTransactionData(null);
setTransactionError('Invalid JSON format.');
+ setAlert({ isOpen: true, message: 'Invalid JSON format.' });
}
};
reader.readAsText(file);
@@ -445,6 +455,39 @@ function Dashboard() {
setJsonFileName(''); // Clear if the file is not JSON
setTransactionData(null);
setTransactionError('Please upload a JSON file.');
+ setAlert({ isOpen: true, message: 'Please upload a valid JSON file.'
});
+ }
+ };
+
+ // Function to handle file upload for key pairs
+ const handleKeyPairFileUpload = (e) => {
+ const file = e.target.files[0];
+ if (file && file.type === 'application/json') {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ try {
+ const uploadedKeyPairs = JSON.parse(event.target.result);
+
+ // Ensure the uploaded data is either an array or an object
+ if (Array.isArray(uploadedKeyPairs)) {
+ appendKeyPairs(uploadedKeyPairs);
+ // After appending, the context sets the
selectedKeyPairIndex to the last one
+ } else if (uploadedKeyPairs.publicKey &&
uploadedKeyPairs.privateKey) {
+ appendKeyPairs([uploadedKeyPairs]); // Wrap single key
pair into an array
+ // After appending, the context sets the
selectedKeyPairIndex to the last one
+ } else {
+ console.error('Invalid JSON format for key pairs.');
+ setAlert({ isOpen: true, message: 'Invalid JSON format
for key pairs.' });
+ }
+ } catch (err) {
+ console.error('Error parsing JSON:', err);
+ setAlert({ isOpen: true, message: 'Error parsing JSON
file.' });
+ }
+ };
+ reader.readAsText(file);
+ } else {
+ console.error('Please upload a valid JSON file.');
+ setAlert({ isOpen: true, message: 'Please upload a valid JSON
file.' });
}
};
@@ -476,11 +519,13 @@ function Dashboard() {
} else {
setTransactionData(null);
setTransactionError('Invalid JSON format: Missing required
fields.');
+ setAlert({ isOpen: true, message: 'Invalid JSON format:
Missing required fields.' });
}
} catch (err) {
console.error('Error parsing JSON:', err);
setTransactionData(null);
setTransactionError('Invalid JSON format.');
+ setAlert({ isOpen: true, message: 'Invalid JSON format.' });
}
};
reader.readAsText(file);
@@ -488,20 +533,27 @@ function Dashboard() {
setJsonFileName('');
setTransactionData(null);
setTransactionError('Please upload a JSON file.');
+ setAlert({ isOpen: true, message: 'Please upload a valid JSON file.'
});
}
};
const handleFileClick = () => {
- fileInputRef.current.click(); // Open file explorer when clicking on
the field
+ fileInputRef.current.click();
+ };
+
+ const handleKeyPairFileClick = () => {
+ keyPairFileInputRef.current.click();
};
const handleSubmit = () => {
if (!transactionData) {
setTransactionError('No valid transaction data found.');
+ setAlert({ isOpen: true, message: 'No valid transaction data found.'
});
return;
}
if (!isConnected) {
setTransactionError('Please connect to a net before submitting a
transaction.');
+ setAlert({ isOpen: true, message: 'Please connect to a net before
submitting a transaction.' });
return;
}
@@ -520,11 +572,12 @@ function Dashboard() {
setTransactionData(null);
} else {
setTransactionError(response.error || 'Transaction submission
failed.');
+ setAlert({ isOpen: true, message: response.error || 'Transaction
submission failed.' });
}
});
};
- // New function to handle transaction ID click
+ // Function to handle transaction ID click
const handleIdClick = () => {
try {
const transactionId = (successResponse &&
successResponse.postTransaction && successResponse.postTransaction.id) || '';
@@ -539,11 +592,12 @@ function Dashboard() {
setIsIdCopied(false);
}, 1500);
} catch (err) {
+ setAlert({ isOpen: true, message: 'Unable to copy transaction ID.' });
console.error('Unable to copy text: ', err);
}
};
- // **New function to handle favicon load error**
+ // Function to handle favicon load error
const handleFaviconError = () => {
setFaviconUrl(''); // This will trigger the globe icon to display
};
@@ -570,11 +624,15 @@ function Dashboard() {
const handleGenerateKeyPair = () => {
generateKeyPair(() => {
- setSelectedKeyPairIndex(keyPairs.length); // Select the newly
generated key pair
+ setSelectedKeyPairIndex(keyPairs.length - 1); // Select the newly
generated key pair
disconnectDueToKeysChange();
});
};
-
+
+ // Function to close the alert modal
+ const closeAlertModal = () => {
+ setAlert({ isOpen: false, message: '' });
+ };
return (
<>
@@ -594,6 +652,7 @@ function Dashboard() {
<button
style={{ background: 'none', color: 'white', fontWeight:
'bolder', outline: 'none', borderStyle: 'none', cursor: 'pointer' }}
onClick={back}
+ title="Logout"
>
<ExitToAppIcon />
</button>
@@ -623,7 +682,7 @@ function Dashboard() {
<tr key={net.name}>
<td>{net.name}</td>
<td>
- <button className="icon-button" onClick={() =>
removeNet(net.name)}>
+ <button className="icon-button" onClick={() =>
removeNet(net.name)} title="Delete Net">
<i className="fas fa-trash"></i>
</button>
</td>
@@ -644,10 +703,10 @@ function Dashboard() {
</div>
</div>
<div className="save-container">
- <button onClick={addNet} className="button-save">
+ <button onClick={addNet} className="button-save"
title="Save Net">
Save
</button>
- <button onClick={() => setShowModal(false)}
className="button-close">
+ <button onClick={() => setShowModal(false)}
className="button-close" title="Close Modal">
Close
</button>
</div>
@@ -665,25 +724,25 @@ function Dashboard() {
{/* Extract transaction ID */}
{successResponse && successResponse.postTransaction &&
successResponse.postTransaction.id ? (
<div className="fieldset">
- <div className="radio-option radio-option--full">
- <input
- type="radio"
- name="transactionId"
- id="txId"
- value={successResponse.postTransaction.id}
- checked
- readOnly
- onClick={handleIdClick}
- />
- <label htmlFor="txId">
- <span>{isIdCopied ? 'Copied' :
`${successResponse.postTransaction.id.slice(0,
5)}...${successResponse.postTransaction.id.slice(-5)}`}</span>
- </label>
- </div>
+ <div className="radio-option radio-option--full">
+ <input
+ type="radio"
+ name="transactionId"
+ id="txId"
+ value={successResponse.postTransaction.id}
+ checked
+ readOnly
+ onClick={handleIdClick}
+ />
+ <label htmlFor="txId">
+ <span>{isIdCopied ? 'Copied' :
`${successResponse.postTransaction.id.slice(0,
5)}...${successResponse.postTransaction.id.slice(-5)}`}</span>
+ </label>
+ </div>
</div>
) : (
<p>No transaction ID found.</p>
)}
- <button onClick={() => setShowSuccessModal(false)}
className="button-close">
+ <button onClick={() => setShowSuccessModal(false)}
className="button-close" title="Close Modal">
Close
</button>
</div>
@@ -691,6 +750,23 @@ function Dashboard() {
</div>
)}
+ {/* Alert Modal */}
+ {alert.isOpen && (
+ <div className="overlay">
+ <div className="modal">
+ <div className="modal-content">
+ <h2>Alert</h2>
+ <p>{alert.message}</p>
+ <div className="save-container">
+ <button onClick={closeAlertModal}
className="button-save" title="Okay">
+ Okay
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ )}
+
<div className="page__content page__content--with-header
page__content--with-bottom-nav">
<h2 className="page__title">Dashboard</h2>
@@ -722,7 +798,7 @@ function Dashboard() {
<span className="status-dot"></span>
<span className="tooltip">{isConnected ? 'Connected' :
'Disconnected'}</span>
</div>
- {selectedNet === 'Custom Net' && (
+ {selectedNet === 'Custom URL' && (
<input
type="text"
value={customUrl}
@@ -758,7 +834,6 @@ function Dashboard() {
</div>
</div>
-
<h2 className="page__title">Select Account</h2>
<div className="net">
@@ -767,7 +842,7 @@ function Dashboard() {
<div className="select-wrapper">
<select
value={selectedKeyPairIndex}
- onChange={(e) => switchKeyPair(e.target.value)}
+ onChange={(e) =>
switchKeyPair(Number(e.target.value))}
className="select"
>
{keyPairs.map((keyPair, index) => (
@@ -780,25 +855,44 @@ function Dashboard() {
</div>
<div className="keypair-icons">
{keyPairs.length > 1 && (
- <button onClick={() => setShowDeleteModal(true)}
className="icon-button">
+ <button onClick={() => setShowDeleteModal(true)}
className="icon-button" title="Delete Key Pair">
<DeleteIcon style={{ color: 'white' }} />
</button>
)}
- <button onClick={handleCopyPublicKey}
className="icon-button">
+ <button onClick={handleCopyPublicKey}
className="icon-button" title="Copy Public Key">
<ContentCopyIcon style={{ color: isCopied ?
'grey' : 'white' }} />
</button>
- <button onClick={handleDownloadKeyPair}
className="icon-button">
+ <button onClick={handleDownloadKeyPair}
className="icon-button" title="Download Key Pair">
<DownloadIcon style={{ color: 'white' }} />
</button>
</div>
</div>
<div className="keypair-actions">
- <button onClick={handleGenerateKeyPair}
className="badge-button">
- Generate <AddCircleOutlineIcon style={{ color:
'white' }} />
- </button>
- <button onClick={handleDownloadAllKeyPairs}
className="badge-button">
- Export <SaveAltIcon style={{ color: 'white' }} />
- </button>
+ <div className="button-with-tooltip">
+ <button onClick={handleGenerateKeyPair}
className="badge-button centered-icon" title="Generate Keys">
+ <AddCircleOutlineIcon style={{ color: 'white'
}} />
+ </button>
+ <span className="tooltip-text">Generate Keys</span>
+ </div>
+ <div className="button-with-tooltip">
+ <button onClick={handleDownloadAllKeyPairs}
className="badge-button centered-icon" title="Export All Keys">
+ <SaveAltIcon style={{ color: 'white' }} />
+ </button>
+ <span className="tooltip-text">Export All
Keys</span>
+ </div>
+ <div className="button-with-tooltip">
+ <button onClick={handleKeyPairFileClick}
className="badge-button centered-icon" title="Upload Keys">
+ <UploadIcon style={{ color: 'white' }} />
+ </button>
+ <span className="tooltip-text">Upload Keys</span>
+ </div>
+ <input
+ type="file"
+ ref={keyPairFileInputRef}
+ style={{ display: 'none' }}
+ accept="application/json"
+ onChange={handleKeyPairFileUpload}
+ />
</div>
</div>
</div>
@@ -810,10 +904,10 @@ function Dashboard() {
<h2>Are you sure?</h2>
<p>This action is irreversible and will delete the
selected key pair forever.</p>
<div className="save-container">
- <button onClick={handleDeleteKeyPair}
className="button-save">
+ <button onClick={handleDeleteKeyPair}
className="button-save" title="Delete Key Pair">
Delete
</button>
- <button onClick={() =>
setShowDeleteModal(false)} className="button-close">
+ <button onClick={() =>
setShowDeleteModal(false)} className="button-close" title="Cancel">
Cancel
</button>
</div>
@@ -822,7 +916,7 @@ function Dashboard() {
</div>
)}
- <button className="button button--full button--main open-popup"
onClick={handleSubmit}>
+ <button className="button button--full button--main open-popup"
onClick={handleSubmit} title="Submit Transaction">
Submit
</button>
<p className="bottom-navigation" style={{ backgroundColor:
'transparent', display: 'flex', justifyContent: 'center', textShadow: '1px 1px
1px rgba(0, 0, 0, 0.3)', color: 'rgb(255, 255, 255, 0.5)', fontSize: '9px' }}>
@@ -832,6 +926,7 @@ function Dashboard() {
</div>
</>
);
+
}
export default Dashboard;
\ No newline at end of file