Add scheduled requests feature with navigation, forms, and table components
Introduced a new 'Solicitudes Programadas' section with full CRUD functionality using localStorage for persistence. Added navigation to toggle between dashboard and scheduled views. Updated the header, sidebar, and main content to support dynamic titles and active views. Included components for viewing and managing scheduled requests (form and table). Enhanced styles for the new section.
This commit is contained in:
30
src/App.js
30
src/App.js
@@ -4,27 +4,41 @@ import { Sidebar, Header } from './components/Layout';
|
|||||||
import { DashboardCards } from './components/Dashboard';
|
import { DashboardCards } from './components/Dashboard';
|
||||||
import RequestTable from './components/Requests/RequestTable';
|
import RequestTable from './components/Requests/RequestTable';
|
||||||
import RequestForm from './components/Requests/RequestForm';
|
import RequestForm from './components/Requests/RequestForm';
|
||||||
|
import ScheduledPage from './components/Scheduled/ScheduledPage';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
const [activeView, setActiveView] = useState('dashboard'); // 'dashboard' | 'scheduled'
|
||||||
|
|
||||||
const handleToggleMobileMenu = () => setIsMobileMenuOpen(!isMobileMenuOpen);
|
const handleToggleMobileMenu = () => setIsMobileMenuOpen(!isMobileMenuOpen);
|
||||||
|
const handleNavigate = (key) => {
|
||||||
|
setActiveView(key);
|
||||||
|
setIsMobileMenuOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerTitle = activeView === 'scheduled' ? 'Solicitudes Programadas' : 'Dashboard';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
isMobileMenuOpen={isMobileMenuOpen}
|
isMobileMenuOpen={isMobileMenuOpen}
|
||||||
onToggleMobileMenu={handleToggleMobileMenu}
|
onToggleMobileMenu={handleToggleMobileMenu}
|
||||||
|
activeView={activeView}
|
||||||
|
onNavigate={handleNavigate}
|
||||||
/>
|
/>
|
||||||
<main className="main-content">
|
<main className="main-content">
|
||||||
<Header />
|
<Header title={headerTitle} />
|
||||||
<section className="content-wrapper">
|
{activeView === 'scheduled' ? (
|
||||||
<DashboardCards />
|
<ScheduledPage />
|
||||||
<div className="content-grid">
|
) : (
|
||||||
<RequestForm />
|
<section className="content-wrapper">
|
||||||
<RequestTable />
|
<DashboardCards />
|
||||||
</div>
|
<div className="content-grid">
|
||||||
</section>
|
<RequestForm />
|
||||||
|
<RequestTable />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import '../../styles/components.css';
|
import '../../styles/components.css';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = ({ title = 'Dashboard' }) => {
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('auth', 'false');
|
localStorage.setItem('auth', 'false');
|
||||||
@@ -10,11 +10,13 @@ const Header = () => {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const iconClass = title === 'Solicitudes Programadas' ? 'fas fa-calendar-alt' : 'fas fa-tachometer-alt';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="page-title">
|
<div className="page-title">
|
||||||
<i className="fas fa-tachometer-alt"></i>
|
<i className={iconClass}></i>
|
||||||
<span>Dashboard</span>
|
<span>{title}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="header-actions">
|
<div className="header-actions">
|
||||||
<div className="search-box">
|
<div className="search-box">
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import '../../styles/components.css';
|
import '../../styles/components.css';
|
||||||
|
|
||||||
const Sidebar = ({ isMobileMenuOpen, onToggleMobileMenu }) => {
|
const Sidebar = ({ isMobileMenuOpen, onToggleMobileMenu, activeView = 'dashboard', onNavigate }) => {
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ icon: 'fas fa-tachometer-alt', label: 'Dashboard', active: true, badge: null },
|
{ key: 'dashboard', icon: 'fas fa-tachometer-alt', label: 'Dashboard', badge: null },
|
||||||
{ icon: 'fas fa-list', label: 'Solicitudes Programadas', active: false, badge: '12' },
|
{ key: 'scheduled', icon: 'fas fa-list', label: 'Solicitudes Programadas', badge: null },
|
||||||
{ icon: 'fas fa-plus-circle', label: 'Crear Solicitud', active: false, badge: null },
|
{ key: 'create', icon: 'fas fa-plus-circle', label: 'Crear Solicitud', badge: null },
|
||||||
{ icon: 'fas fa-history', label: 'Historial', active: false, badge: null },
|
{ key: 'history', icon: 'fas fa-history', label: 'Historial', badge: null },
|
||||||
{ icon: 'fas fa-cog', label: 'Configuración', active: false, badge: null },
|
{ key: 'settings', icon: 'fas fa-cog', label: 'Configuración', badge: null },
|
||||||
{ icon: 'fas fa-question-circle', label: 'Ayuda', active: false, badge: null }
|
{ key: 'help', icon: 'fas fa-question-circle', label: 'Ayuda', badge: null }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleClick = (e, key) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onNavigate?.(key);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@@ -33,13 +38,13 @@ const Sidebar = ({ isMobileMenuOpen, onToggleMobileMenu }) => {
|
|||||||
|
|
||||||
<div className="nav-container">
|
<div className="nav-container">
|
||||||
<ul className="nav-links">
|
<ul className="nav-links">
|
||||||
{navItems.map((item, index) => (
|
{navItems.map((item) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={item.key}
|
||||||
className={item.active ? 'active' : ''}
|
className={activeView === item.key ? 'active' : ''}
|
||||||
data-tooltip={item.label}
|
data-tooltip={item.label}
|
||||||
>
|
>
|
||||||
<a href="#">
|
<a href="#" onClick={(e) => handleClick(e, item.key)}>
|
||||||
<i className={item.icon}></i>
|
<i className={item.icon}></i>
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
{item.badge && <span className="badge">{item.badge}</span>}
|
{item.badge && <span className="badge">{item.badge}</span>}
|
||||||
|
|||||||
135
src/components/Scheduled/ScheduledForm.js
Normal file
135
src/components/Scheduled/ScheduledForm.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import '../../styles/components.css';
|
||||||
|
|
||||||
|
const defaultItem = {
|
||||||
|
id: null,
|
||||||
|
url: '',
|
||||||
|
method: 'GET',
|
||||||
|
headers: '',
|
||||||
|
body: '',
|
||||||
|
scheduleType: 'cron', // 'cron' | 'once'
|
||||||
|
cronExpr: '0 0 * * *',
|
||||||
|
datetime: '',
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ScheduledForm = ({ editing, onSave, onCancel }) => {
|
||||||
|
const [item, setItem] = useState(defaultItem);
|
||||||
|
const isEditing = useMemo(() => Boolean(editing && editing.id), [editing]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editing) {
|
||||||
|
setItem({ ...defaultItem, ...editing });
|
||||||
|
} else {
|
||||||
|
setItem(defaultItem);
|
||||||
|
}
|
||||||
|
}, [editing]);
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setItem((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// simple validations
|
||||||
|
if (!item.url) return alert('La URL es obligatoria');
|
||||||
|
if (item.scheduleType === 'cron' && !item.cronExpr) return alert('La expresión CRON es obligatoria');
|
||||||
|
if (item.scheduleType === 'once' && !item.datetime) return alert('La fecha y hora son obligatorias');
|
||||||
|
|
||||||
|
const parsedHeaders = safeParseJSON(item.headers);
|
||||||
|
const parsedBody = safeParseJSON(item.body);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...item,
|
||||||
|
headers: parsedHeaders,
|
||||||
|
body: parsedBody,
|
||||||
|
};
|
||||||
|
onSave?.(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeParseJSON = (text) => {
|
||||||
|
if (!text) return undefined;
|
||||||
|
try { return JSON.parse(text); } catch (_) { return text; }
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form-container">
|
||||||
|
<div className="form-header">
|
||||||
|
<h3>
|
||||||
|
<i className="fas fa-calendar-plus"></i>
|
||||||
|
{isEditing ? 'Editar programación' : 'Nueva programación'}
|
||||||
|
</h3>
|
||||||
|
{isEditing && (
|
||||||
|
<button type="button" className="btn btn-outline btn-sm" onClick={() => onCancel?.()}>
|
||||||
|
<i className="fas fa-times"></i> Cancelar edición
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleSubmit} noValidate>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="url">URL</label>
|
||||||
|
<input id="url" name="url" type="url" placeholder="https://api.tu-dominio.com/recurso" value={item.url} onChange={handleChange} />
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="method">Método</label>
|
||||||
|
<select id="method" name="method" value={item.method} onChange={handleChange}>
|
||||||
|
<option>GET</option>
|
||||||
|
<option>POST</option>
|
||||||
|
<option>PUT</option>
|
||||||
|
<option>PATCH</option>
|
||||||
|
<option>DELETE</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="scheduleType">Tipo de programación</label>
|
||||||
|
<select id="scheduleType" name="scheduleType" value={item.scheduleType} onChange={handleChange}>
|
||||||
|
<option value="cron">CRON</option>
|
||||||
|
<option value="once">Única vez</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.scheduleType === 'cron' ? (
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="cronExpr">Expresión CRON</label>
|
||||||
|
<input id="cronExpr" name="cronExpr" type="text" placeholder="* * * * *" value={item.cronExpr} onChange={handleChange} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="datetime">Fecha y hora (UTC)</label>
|
||||||
|
<input id="datetime" name="datetime" type="datetime-local" value={item.datetime} onChange={handleChange} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="headers">Headers (JSON opcional)</label>
|
||||||
|
<textarea id="headers" name="headers" placeholder='{"Authorization":"Bearer ..."}' value={item.headers} onChange={handleChange} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="body">Body (JSON opcional)</label>
|
||||||
|
<textarea id="body" name="body" placeholder='{"campo":"valor"}' value={item.body} onChange={handleChange} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="enabled">Estado</label>
|
||||||
|
<select id="enabled" name="enabled" value={item.enabled ? 'true' : 'false'} onChange={(e) => setItem((p) => ({ ...p, enabled: e.target.value === 'true' }))}>
|
||||||
|
<option value="true">Activa</option>
|
||||||
|
<option value="false">Pausada</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
<i className="fas fa-save"></i>
|
||||||
|
{isEditing ? 'Guardar cambios' : 'Crear programación'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScheduledForm;
|
||||||
130
src/components/Scheduled/ScheduledPage.js
Normal file
130
src/components/Scheduled/ScheduledPage.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import '../../styles/components.css';
|
||||||
|
import ScheduledForm from './ScheduledForm';
|
||||||
|
import ScheduledTable from './ScheduledTable';
|
||||||
|
|
||||||
|
const storageKey = 'scheduled.requests.v1';
|
||||||
|
|
||||||
|
const nowPlusMinutes = (m) => {
|
||||||
|
const d = new Date(Date.now() + m * 60000);
|
||||||
|
return d.toISOString().slice(0,16).replace('T', ' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFromStorage = () => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(storageKey);
|
||||||
|
if (!raw) return null;
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (_e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveToStorage = (items) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(items));
|
||||||
|
} catch (_e) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const seeded = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
url: 'https://api.ejemplo.com/reportes/daily',
|
||||||
|
method: 'GET',
|
||||||
|
headers: undefined,
|
||||||
|
body: undefined,
|
||||||
|
scheduleType: 'cron',
|
||||||
|
cronExpr: '0 6 * * *',
|
||||||
|
datetime: '',
|
||||||
|
nextRun: nowPlusMinutes(60 * 12),
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
url: 'https://api.ejemplo.com/users/sync',
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-Env': 'prod' },
|
||||||
|
body: { force: true },
|
||||||
|
scheduleType: 'once',
|
||||||
|
cronExpr: '',
|
||||||
|
datetime: nowPlusMinutes(120),
|
||||||
|
nextRun: nowPlusMinutes(120),
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ScheduledPage = () => {
|
||||||
|
const [items, setItems] = useState(() => loadFromStorage() || seeded);
|
||||||
|
const [editing, setEditing] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
saveToStorage(items);
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
const countEnabled = useMemo(() => items.filter(i => i.enabled).length, [items]);
|
||||||
|
|
||||||
|
const handleSave = (payload) => {
|
||||||
|
if (payload.id) {
|
||||||
|
setItems((prev) => prev.map((it) => (it.id === payload.id ? { ...it, ...payload } : it)));
|
||||||
|
setEditing(null);
|
||||||
|
} else {
|
||||||
|
const nextId = items.length ? Math.max(...items.map(i => i.id)) + 1 : 1;
|
||||||
|
const next = {
|
||||||
|
...payload,
|
||||||
|
id: nextId,
|
||||||
|
nextRun: payload.scheduleType === 'cron' ? nowPlusMinutes(60 * 24) : (payload.datetime || nowPlusMinutes(5)),
|
||||||
|
};
|
||||||
|
setItems((prev) => [next, ...prev]);
|
||||||
|
setEditing(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleEnabled = (item) => {
|
||||||
|
setItems((prev) => prev.map((it) => it.id === item.id ? { ...it, enabled: !it.enabled } : it));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRunNow = (item) => {
|
||||||
|
alert(`Se ejecutó "${item.method} ${item.url}" (simulado).`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (item) => {
|
||||||
|
if (window.confirm('¿Eliminar esta programación?')) {
|
||||||
|
setItems((prev) => prev.filter((it) => it.id !== item.id));
|
||||||
|
if (editing && editing.id === item.id) setEditing(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="content-wrapper">
|
||||||
|
<div className="card" style={{ marginBottom: 24 }}>
|
||||||
|
<div className="form-header">
|
||||||
|
<h3>
|
||||||
|
<i className="fas fa-calendar-alt"></i>
|
||||||
|
Solicitudes Programadas
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<span style={{ color: 'var(--gray)' }}>
|
||||||
|
Activas: {countEnabled} / {items.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style={{ color: 'var(--gray)' }}>
|
||||||
|
Administra las ejecuciones automáticas de tus solicitudes HTTP.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="content-grid">
|
||||||
|
<ScheduledForm editing={editing} onSave={handleSave} onCancel={() => setEditing(null)} />
|
||||||
|
<ScheduledTable
|
||||||
|
items={items}
|
||||||
|
onToggleEnabled={handleToggleEnabled}
|
||||||
|
onRunNow={handleRunNow}
|
||||||
|
onEdit={setEditing}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScheduledPage;
|
||||||
84
src/components/Scheduled/ScheduledTable.js
Normal file
84
src/components/Scheduled/ScheduledTable.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import '../../styles/components.css';
|
||||||
|
|
||||||
|
const ScheduledTable = ({ items = [], onToggleEnabled, onRunNow, onEdit, onDelete }) => {
|
||||||
|
const getStatusPill = (item) => {
|
||||||
|
const cls = `status ${item.enabled ? 'completed' : 'pending'}`;
|
||||||
|
const icon = item.enabled ? 'fas fa-check' : 'fas fa-pause';
|
||||||
|
return (
|
||||||
|
<span className={cls}>
|
||||||
|
<i className={icon}></i> {item.enabled ? 'Activa' : 'Pausada'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
return (
|
||||||
|
<div className="card" role="status">
|
||||||
|
<div className="form-header">
|
||||||
|
<h3>
|
||||||
|
<i className="fas fa-calendar-alt"></i>
|
||||||
|
Programaciones
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p style={{ color: 'var(--gray)' }}>No hay solicitudes programadas todavía. Crea una nueva para comenzar.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="table-container">
|
||||||
|
<div className="table-header">
|
||||||
|
<h3>Solicitudes Programadas</h3>
|
||||||
|
<div className="table-actions">
|
||||||
|
<button className="btn btn-outline btn-sm" onClick={() => window.location.reload()}>
|
||||||
|
<i className="fas fa-sync"></i> Actualizar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>URL</th>
|
||||||
|
<th>Método</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Cron / Fecha-Hora</th>
|
||||||
|
<th>Próxima ejecución</th>
|
||||||
|
<th>Estado</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{items.map((item) => (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>#{String(item.id).padStart(5, '0')}</td>
|
||||||
|
<td>{item.url}</td>
|
||||||
|
<td>{item.method}</td>
|
||||||
|
<td>{item.scheduleType === 'cron' ? 'CRON' : 'Única'}</td>
|
||||||
|
<td>{item.scheduleType === 'cron' ? item.cronExpr : item.datetime}</td>
|
||||||
|
<td>{item.nextRun || '—'}</td>
|
||||||
|
<td>{getStatusPill(item)}</td>
|
||||||
|
<td className="actions">
|
||||||
|
<button className="view" title="Editar" onClick={() => onEdit?.(item)}>
|
||||||
|
<i className="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button className="execute" title="Ejecutar ahora" onClick={() => onRunNow?.(item)}>
|
||||||
|
<i className="fas fa-play"></i>
|
||||||
|
</button>
|
||||||
|
<button className="cancel" title={item.enabled ? 'Pausar' : 'Activar'} onClick={() => onToggleEnabled?.(item)}>
|
||||||
|
<i className={item.enabled ? 'fas fa-pause' : 'fas fa-play'}></i>
|
||||||
|
</button>
|
||||||
|
<button className="cancel" title="Eliminar" onClick={() => onDelete?.(item)}>
|
||||||
|
<i className="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScheduledTable;
|
||||||
Reference in New Issue
Block a user