746 lines
25 KiB
JavaScript
746 lines
25 KiB
JavaScript
import React, { useState, useEffect } from "react";
|
|
import {
|
|
Button,
|
|
Modal,
|
|
FormControl,
|
|
InputLabel,
|
|
Select,
|
|
MenuItem,
|
|
TextField,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Paper,
|
|
Pagination,
|
|
IconButton,
|
|
Checkbox,
|
|
FormControlLabel,
|
|
Chip,
|
|
Box,
|
|
Typography,
|
|
Card,
|
|
CardHeader,
|
|
CardContent,
|
|
Divider,
|
|
Grid,
|
|
TextareaAutosize,
|
|
Popover,
|
|
List,
|
|
ListItem,
|
|
ListItemText,
|
|
ListItemIcon,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
DialogContentText,
|
|
CircularProgress
|
|
} from "@mui/material";
|
|
import {
|
|
Edit as EditIcon,
|
|
Delete as DeleteIcon,
|
|
Add as AddIcon,
|
|
Menu as MenuIcon,
|
|
Close as CloseIcon,
|
|
Search as SearchIcon,
|
|
LibraryBooks as LibraryBooksIcon,
|
|
CheckCircle as CheckCircleIcon
|
|
} from "@mui/icons-material";
|
|
import { toast } from "react-toastify";
|
|
import * as tokenRegistryAPI from './tokenregistryapi';
|
|
|
|
function TOKENRegistry() {
|
|
const [tokens, setTokens] = useState([]);
|
|
const [showAddEditModal, setShowAddEditModal] = useState(false);
|
|
const [showGenerateTokenModal, setShowGenerateTokenModal] = useState(false);
|
|
const [newTokenName, setNewTokenName] = useState("");
|
|
const [generatedToken, setGeneratedToken] = useState("");
|
|
const [currentToken, setCurrentToken] = useState({
|
|
id: "",
|
|
tokenName: "",
|
|
tokenValue: "",
|
|
isActive: true,
|
|
scopes: []
|
|
});
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [recordsPerPage, setRecordsPerPage] = useState(10);
|
|
const [visibleColumns, setVisibleColumns] = useState({
|
|
id: true,
|
|
tokenName: true,
|
|
tokenValue: true,
|
|
scopes: true,
|
|
isActive: true,
|
|
actions: true
|
|
});
|
|
const [loading, setLoading] = useState(true);
|
|
const [selectedScope, setSelectedScope] = useState("");
|
|
const [selectedScopes, setSelectedScopes] = useState([]);
|
|
const [availableScopes] = useState([
|
|
{ value: 'read', label: 'Read Access' },
|
|
{ value: 'write', label: 'Write Access' },
|
|
{ value: 'delete', label: 'Delete Access' },
|
|
{ value: 'admin', label: 'Admin Access' },
|
|
]);
|
|
const [anchorEl, setAnchorEl] = useState(null);
|
|
const [columnsAnchorEl, setColumnsAnchorEl] = useState(null);
|
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
|
const [tokenToDelete, setTokenToDelete] = useState(null);
|
|
|
|
useEffect(() => {
|
|
fetchTokens();
|
|
}, []);
|
|
|
|
const fetchTokens = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await tokenRegistryAPI.fetchAllTokens();
|
|
setTokens(data);
|
|
} catch (error) {
|
|
handleApiError(error, "fetch tokens");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleApiError = (error, action) => {
|
|
if (error.message === 'Unauthorized') {
|
|
toast.error("Your session has expired. Please log in again.");
|
|
} else {
|
|
toast.error(`Failed to ${action}`);
|
|
console.error(`Error ${action}:`, error);
|
|
}
|
|
};
|
|
|
|
const toggleColumn = (column) => {
|
|
setVisibleColumns(prev => ({
|
|
...prev,
|
|
[column]: !prev[column],
|
|
}));
|
|
};
|
|
|
|
const handleInputChange = (event) => {
|
|
const { name, value, type, checked } = event.target;
|
|
setCurrentToken(prev => ({
|
|
...prev,
|
|
[name]: type === "checkbox" ? checked : value
|
|
}));
|
|
};
|
|
|
|
const handleSearch = (query) => {
|
|
setSearchQuery(query);
|
|
setCurrentPage(1);
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setShowAddEditModal(false);
|
|
setShowGenerateTokenModal(false);
|
|
setGeneratedToken("");
|
|
setNewTokenName("");
|
|
setSelectedScopes([]);
|
|
};
|
|
|
|
const handleRecordsPerPageChange = (number) => {
|
|
setRecordsPerPage(number);
|
|
setCurrentPage(1);
|
|
setAnchorEl(null);
|
|
};
|
|
|
|
const filteredTokens = tokens.filter(
|
|
(item) =>
|
|
item.tokenName &&
|
|
item.tokenName.toLowerCase().includes(searchQuery.toLowerCase())
|
|
);
|
|
|
|
const totalPages = Math.ceil(filteredTokens.length / recordsPerPage);
|
|
const handlePageChange = (event, pageNumber) => {
|
|
setCurrentPage(pageNumber);
|
|
};
|
|
|
|
const slicedTokens = filteredTokens.slice(
|
|
(currentPage - 1) * recordsPerPage,
|
|
currentPage * recordsPerPage
|
|
);
|
|
|
|
const handleSubmit = async (event) => {
|
|
event.preventDefault();
|
|
try {
|
|
if (isEditing) {
|
|
await tokenRegistryAPI.updateToken(currentToken.id, currentToken);
|
|
toast.success("Token updated successfully!");
|
|
} else {
|
|
await tokenRegistryAPI.createToken(currentToken);
|
|
toast.success("Token added successfully!");
|
|
}
|
|
setShowAddEditModal(false);
|
|
fetchTokens();
|
|
} catch (error) {
|
|
handleApiError(error, isEditing ? "update token" : "add token");
|
|
}
|
|
};
|
|
|
|
const openModal = (token = { id: "", tokenName: "", tokenValue: "", isActive: false, scopes: [] }) => {
|
|
setIsEditing(!!token.id);
|
|
setCurrentToken(token);
|
|
setSelectedScopes(token.scopes || []);
|
|
setShowAddEditModal(true);
|
|
};
|
|
|
|
const handleDeleteClick = (id) => {
|
|
setTokenToDelete(id);
|
|
setDeleteConfirmOpen(true);
|
|
};
|
|
|
|
const handleDeleteConfirm = async () => {
|
|
try {
|
|
await tokenRegistryAPI.deleteToken(tokenToDelete);
|
|
toast.success('Token deleted successfully');
|
|
fetchTokens();
|
|
} catch (error) {
|
|
handleApiError(error, "delete token");
|
|
} finally {
|
|
setDeleteConfirmOpen(false);
|
|
setTokenToDelete(null);
|
|
}
|
|
};
|
|
|
|
const addScope = () => {
|
|
if (selectedScope && !selectedScopes.includes(selectedScope)) {
|
|
setSelectedScopes([...selectedScopes, selectedScope]);
|
|
setSelectedScope("");
|
|
}
|
|
};
|
|
|
|
const removeScope = (scopeToRemove) => {
|
|
setSelectedScopes(selectedScopes.filter(scope => scope !== scopeToRemove));
|
|
};
|
|
|
|
const generateNewToken = async () => {
|
|
if (!newTokenName.trim()) {
|
|
toast.error("Please enter a token name");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const data = await tokenRegistryAPI.generateToken({
|
|
name: newTokenName,
|
|
scopes: selectedScopes
|
|
});
|
|
setGeneratedToken(data.tokenValue);
|
|
toast.success("Token generated successfully!");
|
|
fetchTokens();
|
|
} catch (error) {
|
|
handleApiError(error, "generate token");
|
|
}
|
|
};
|
|
|
|
const handleMenuClick = (event) => {
|
|
setAnchorEl(event.currentTarget);
|
|
};
|
|
|
|
const handleMenuClose = () => {
|
|
setAnchorEl(null);
|
|
};
|
|
|
|
const handleColumnsMenuClick = (event) => {
|
|
setColumnsAnchorEl(event.currentTarget);
|
|
};
|
|
|
|
const handleColumnsMenuClose = () => {
|
|
setColumnsAnchorEl(null);
|
|
};
|
|
|
|
return (
|
|
<Box sx={{ marginTop: "1rem", padding: 2 }}>
|
|
{loading ? (
|
|
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
|
|
<CircularProgress />
|
|
</Box>
|
|
) : (
|
|
<Box>
|
|
{/* Token Generation Card */}
|
|
<Card sx={{ mb: 4, boxShadow: 3 }}>
|
|
<CardHeader
|
|
title="Token Management"
|
|
action={
|
|
<Button
|
|
variant="contained"
|
|
onClick={() => setShowGenerateTokenModal(true)}
|
|
sx={{ backgroundColor: '#1976d2', color: 'white' }}
|
|
>
|
|
Generate New Token
|
|
</Button>
|
|
}
|
|
/>
|
|
<CardContent>
|
|
<Typography variant="body1">
|
|
Manage your API tokens here. Generate new tokens or delete existing ones.
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Box display="flex" justifyContent="space-between" alignItems="center" mb={4}>
|
|
<Typography variant="h4" component="h1" sx={{ fontWeight: 'bold' }}>
|
|
Token Registry
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Grid container spacing={2} alignItems="center" mb={3}>
|
|
<Grid item xs={12} md={8} lg={6}>
|
|
<TextField
|
|
fullWidth
|
|
variant="outlined"
|
|
placeholder="Search"
|
|
value={searchQuery}
|
|
onChange={(e) => handleSearch(e.target.value)}
|
|
InputProps={{
|
|
startAdornment: (
|
|
<SearchIcon sx={{ color: 'action.active', mr: 1 }} />
|
|
),
|
|
sx: {
|
|
borderRadius: '10px',
|
|
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
|
|
}
|
|
}}
|
|
/>
|
|
</Grid>
|
|
|
|
<Grid item xs={12} md={4} lg={6} display="flex" justifyContent="flex-end">
|
|
<IconButton
|
|
onClick={() => openModal()}
|
|
color="primary"
|
|
sx={{ mr: 2 }}
|
|
>
|
|
<AddIcon />
|
|
</IconButton>
|
|
<IconButton
|
|
onClick={handleColumnsMenuClick}
|
|
color="primary"
|
|
>
|
|
<MenuIcon />
|
|
</IconButton>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
<TableContainer component={Paper} sx={{ boxShadow: 3 }}>
|
|
<Table>
|
|
<TableHead sx={{ backgroundColor: '#f5f5f5' }}>
|
|
<TableRow>
|
|
{Object.keys(visibleColumns).filter(key => visibleColumns[key]).map(key => (
|
|
<TableCell key={key}>
|
|
{key.charAt(0).toUpperCase() + key.slice(1)}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{slicedTokens.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={Object.keys(visibleColumns).filter(key => visibleColumns[key]).length}
|
|
align="center"
|
|
>
|
|
No Data Available
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
slicedTokens.map((token, index) => (
|
|
<TableRow key={index} hover>
|
|
{Object.keys(visibleColumns).filter(key => visibleColumns[key]).map(key => (
|
|
<TableCell key={key}>
|
|
{key === "actions" ? (
|
|
<>
|
|
<IconButton
|
|
onClick={() => openModal(token)}
|
|
color="primary"
|
|
sx={{ mr: 1 }}
|
|
>
|
|
<EditIcon />
|
|
</IconButton>
|
|
<IconButton
|
|
onClick={() => handleDeleteClick(token.id)}
|
|
color="error"
|
|
>
|
|
<DeleteIcon />
|
|
</IconButton>
|
|
</>
|
|
) : key === "isActive" ? (
|
|
<Chip
|
|
label={token.isActive ? "Active" : "Inactive"}
|
|
color={token.isActive ? "success" : "error"}
|
|
size="small"
|
|
sx={{
|
|
backgroundColor: token.isActive ? '#d4f7d4' : '#f7d4d4',
|
|
color: token.isActive ? 'green' : 'red'
|
|
}}
|
|
/>
|
|
) : key === "scopes" ? (
|
|
<Box>
|
|
{token.scopes && token.scopes.map(scope => {
|
|
const scopeInfo = availableScopes.find(s => s.value === scope);
|
|
return (
|
|
<Chip
|
|
key={scope}
|
|
label={scopeInfo?.label || scope}
|
|
color="primary"
|
|
size="small"
|
|
sx={{ mr: 1, mb: 1 }}
|
|
/>
|
|
);
|
|
})}
|
|
</Box>
|
|
) : (
|
|
token[key]
|
|
)}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
|
|
{/* Manage Columns */}
|
|
<Popover
|
|
open={Boolean(columnsAnchorEl)}
|
|
anchorEl={columnsAnchorEl}
|
|
onClose={handleColumnsMenuClose}
|
|
anchorOrigin={{
|
|
vertical: 'bottom',
|
|
horizontal: 'right',
|
|
}}
|
|
transformOrigin={{
|
|
vertical: 'top',
|
|
horizontal: 'right',
|
|
}}
|
|
>
|
|
<List>
|
|
{Object.keys(visibleColumns).map((column) => (
|
|
<ListItem key={column} dense button onClick={() => toggleColumn(column)}>
|
|
<ListItemIcon>
|
|
<Checkbox
|
|
edge="start"
|
|
checked={visibleColumns[column]}
|
|
tabIndex={-1}
|
|
disableRipple
|
|
/>
|
|
</ListItemIcon>
|
|
<ListItemText primary={column.charAt(0).toUpperCase() + column.slice(1).toLowerCase()} />
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</Popover>
|
|
|
|
{/* Records Per Page */}
|
|
<Box display="flex" justifyContent="flex-end" mt={2}>
|
|
<Button
|
|
variant="outlined"
|
|
startIcon={<LibraryBooksIcon />}
|
|
onClick={handleMenuClick}
|
|
sx={{ border: '2px solid', borderRadius: '8px', boxShadow: 1 }}
|
|
>
|
|
{recordsPerPage}
|
|
</Button>
|
|
<Popover
|
|
open={Boolean(anchorEl)}
|
|
anchorEl={anchorEl}
|
|
onClose={handleMenuClose}
|
|
anchorOrigin={{
|
|
vertical: 'bottom',
|
|
horizontal: 'right',
|
|
}}
|
|
transformOrigin={{
|
|
vertical: 'top',
|
|
horizontal: 'right',
|
|
}}
|
|
>
|
|
<List>
|
|
{[1, 5, 10, 20, 50].map((number) => (
|
|
<ListItem
|
|
key={number}
|
|
dense
|
|
button
|
|
onClick={() => handleRecordsPerPageChange(number)}
|
|
sx={{ minWidth: '100px' }}
|
|
>
|
|
<ListItemText primary={number} />
|
|
{recordsPerPage === number && <CheckCircleIcon color="primary" />}
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</Popover>
|
|
</Box>
|
|
|
|
<Box display="flex" justifyContent="center" mt={2}>
|
|
<Pagination
|
|
count={totalPages}
|
|
page={currentPage}
|
|
onChange={handlePageChange}
|
|
color="primary"
|
|
showFirstButton
|
|
showLastButton
|
|
/>
|
|
</Box>
|
|
|
|
{/* Generate Token Modal */}
|
|
<Dialog open={showGenerateTokenModal} onClose={handleClose} fullWidth maxWidth="sm">
|
|
<DialogTitle>
|
|
Generate New Token
|
|
<IconButton
|
|
onClick={handleClose}
|
|
sx={{
|
|
position: 'absolute',
|
|
right: 8,
|
|
top: 8,
|
|
}}
|
|
>
|
|
<CloseIcon />
|
|
</IconButton>
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
<Box component="form" sx={{ mt: 1 }}>
|
|
<TextField
|
|
margin="normal"
|
|
fullWidth
|
|
label="Token Name"
|
|
value={newTokenName}
|
|
onChange={(e) => setNewTokenName(e.target.value)}
|
|
required
|
|
sx={{ mb: 2 }}
|
|
/>
|
|
|
|
<Typography variant="subtitle1" gutterBottom>
|
|
Token Scopes
|
|
</Typography>
|
|
<Box display="flex" alignItems="center" mb={2}>
|
|
<FormControl fullWidth sx={{ mr: 2 }}>
|
|
<InputLabel>Select a scope</InputLabel>
|
|
<Select
|
|
value={selectedScope}
|
|
onChange={(e) => setSelectedScope(e.target.value)}
|
|
label="Select a scope"
|
|
>
|
|
<MenuItem value=""><em>Select a scope</em></MenuItem>
|
|
{availableScopes.map(scope => (
|
|
<MenuItem key={scope.value} value={scope.value}>
|
|
{scope.label}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
<Button
|
|
variant="outlined"
|
|
onClick={addScope}
|
|
disabled={!selectedScope}
|
|
>
|
|
Add Scope
|
|
</Button>
|
|
</Box>
|
|
<Typography variant="caption" display="block" gutterBottom>
|
|
Select and add the permissions this token should have
|
|
</Typography>
|
|
|
|
{/* Display selected scopes as chips */}
|
|
{selectedScopes.length > 0 && (
|
|
<Box sx={{ mt: 2 }}>
|
|
{selectedScopes.map(scope => {
|
|
const scopeInfo = availableScopes.find(s => s.value === scope);
|
|
return (
|
|
<Chip
|
|
key={scope}
|
|
label={scopeInfo?.label || scope}
|
|
color="primary"
|
|
onDelete={() => removeScope(scope)}
|
|
sx={{ mr: 1, mb: 1 }}
|
|
/>
|
|
);
|
|
})}
|
|
</Box>
|
|
)}
|
|
|
|
{generatedToken && (
|
|
<Box sx={{ mt: 2 }}>
|
|
<Typography variant="subtitle1" gutterBottom>
|
|
Generated Token
|
|
</Typography>
|
|
<TextareaAutosize
|
|
minRows={3}
|
|
value={generatedToken}
|
|
readOnly
|
|
style={{ width: '100%', padding: '8px', borderRadius: '4px', borderColor: '#ccc' }}
|
|
/>
|
|
<Typography variant="caption" display="block" gutterBottom>
|
|
Copy this token and store it securely. You won't be able to see it again.
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={handleClose}>Close</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={generateNewToken}
|
|
disabled={!newTokenName.trim()}
|
|
>
|
|
Generate Token
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
{/* Add/Edit Token Modal */}
|
|
<Dialog open={showAddEditModal} onClose={handleClose} fullWidth maxWidth="sm">
|
|
<DialogTitle>
|
|
{isEditing ? "Edit Token" : "Add Token"}
|
|
<IconButton
|
|
onClick={handleClose}
|
|
sx={{
|
|
position: 'absolute',
|
|
right: 8,
|
|
top: 8,
|
|
}}
|
|
>
|
|
<CloseIcon />
|
|
</IconButton>
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
|
<TextField
|
|
margin="normal"
|
|
fullWidth
|
|
label="Token Name"
|
|
name="tokenName"
|
|
value={currentToken.tokenName}
|
|
onChange={handleInputChange}
|
|
required
|
|
sx={{ mb: 2 }}
|
|
/>
|
|
<TextField
|
|
margin="normal"
|
|
fullWidth
|
|
label="Token Value"
|
|
name="tokenValue"
|
|
value={currentToken.tokenValue}
|
|
onChange={handleInputChange}
|
|
required
|
|
sx={{ mb: 2 }}
|
|
/>
|
|
|
|
<Typography variant="subtitle1" gutterBottom>
|
|
Token Scopes
|
|
</Typography>
|
|
<Box display="flex" alignItems="center" mb={2}>
|
|
<FormControl fullWidth sx={{ mr: 2 }}>
|
|
<InputLabel>Select a scope</InputLabel>
|
|
<Select
|
|
value={selectedScope}
|
|
onChange={(e) => setSelectedScope(e.target.value)}
|
|
label="Select a scope"
|
|
>
|
|
<MenuItem value=""><em>Select a scope</em></MenuItem>
|
|
{availableScopes.map(scope => (
|
|
<MenuItem key={scope.value} value={scope.value}>
|
|
{scope.label}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
<Button
|
|
variant="outlined"
|
|
onClick={() => {
|
|
if (selectedScope && !currentToken.scopes.includes(selectedScope)) {
|
|
setCurrentToken(prev => ({
|
|
...prev,
|
|
scopes: [...prev.scopes, selectedScope]
|
|
}));
|
|
setSelectedScope("");
|
|
}
|
|
}}
|
|
disabled={!selectedScope}
|
|
>
|
|
Add Scope
|
|
</Button>
|
|
</Box>
|
|
<Typography variant="caption" display="block" gutterBottom>
|
|
Select and add the permissions this token should have
|
|
</Typography>
|
|
|
|
{/* Display selected scopes as chips */}
|
|
{currentToken.scopes && currentToken.scopes.length > 0 && (
|
|
<Box sx={{ mt: 2 }}>
|
|
{currentToken.scopes.map(scope => {
|
|
const scopeInfo = availableScopes.find(s => s.value === scope);
|
|
return (
|
|
<Chip
|
|
key={scope}
|
|
label={scopeInfo?.label || scope}
|
|
color="primary"
|
|
onDelete={() => {
|
|
setCurrentToken(prev => ({
|
|
...prev,
|
|
scopes: prev.scopes.filter(s => s !== scope)
|
|
}));
|
|
}}
|
|
sx={{ mr: 1, mb: 1 }}
|
|
/>
|
|
);
|
|
})}
|
|
</Box>
|
|
)}
|
|
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={currentToken.isActive}
|
|
onChange={handleInputChange}
|
|
name="isActive"
|
|
color="primary"
|
|
/>
|
|
}
|
|
label="Active?"
|
|
sx={{ mt: 2 }}
|
|
/>
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={handleClose}>Close</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={handleSubmit}
|
|
type="submit"
|
|
>
|
|
{isEditing ? "Update Token" : "Add Token"}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
<Dialog
|
|
open={deleteConfirmOpen}
|
|
onClose={() => setDeleteConfirmOpen(false)}
|
|
>
|
|
<DialogTitle>Confirm Delete</DialogTitle>
|
|
<DialogContent>
|
|
<DialogContentText>
|
|
Are you sure you want to delete this token?
|
|
</DialogContentText>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => setDeleteConfirmOpen(false)}>Cancel</Button>
|
|
<Button onClick={handleDeleteConfirm} color="error" autoFocus>
|
|
Delete
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export default TOKENRegistry; |