Improve update_token error — show CARD link for assets that cant be actioned via API
When a CARD action fails with 'update_token not found', display a clear message explaining the asset cannot be actioned via API, with a prominent 'Open in CARD (ID copied)' button that copies the host ID to clipboard and opens card.charter.com/ipn-search in a new tab. Applied to both CardDetailModal (reporting page) and CardActionModal (queue).
This commit is contained in:
@@ -353,9 +353,51 @@ export default function CardActionModal({ isOpen, onClose, item, initialAction,
|
|||||||
|
|
||||||
{/* Execution error */}
|
{/* Execution error */}
|
||||||
{execError && (
|
{execError && (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0.75rem', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.3)', borderRadius: '0.375rem', marginBottom: '0.75rem' }}>
|
<div style={{ padding: '0.5rem 0.75rem', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.3)', borderRadius: '0.375rem', marginBottom: '0.75rem' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
<AlertCircle style={{ width: '13px', height: '13px', color: '#EF4444', flexShrink: 0 }} />
|
<AlertCircle style={{ width: '13px', height: '13px', color: '#EF4444', flexShrink: 0 }} />
|
||||||
<span style={{ fontSize: '0.7rem', color: '#FCA5A5' }}>{execError}</span>
|
<span style={{ fontSize: '0.7rem', color: '#FCA5A5' }}>
|
||||||
|
{execError.includes('update_token') ? 'Cannot action via API — this asset has no update token.' : execError}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{execError.includes('update_token') && item?.host_id && (
|
||||||
|
<div style={{ marginTop: '0.4rem', paddingTop: '0.4rem', borderTop: '1px solid rgba(239, 68, 68, 0.2)' }}>
|
||||||
|
<span style={{ fontSize: '0.65rem', color: '#94A3B8', display: 'block', marginBottom: '0.3rem' }}>
|
||||||
|
Action this asset directly in CARD instead:
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
try {
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = String(item.host_id);
|
||||||
|
ta.style.position = 'fixed';
|
||||||
|
ta.style.opacity = '0';
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
} catch (_) { /* best effort */ }
|
||||||
|
window.open('https://card.charter.com/ipn-search', '_blank');
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
display: 'inline-flex', alignItems: 'center', gap: '0.35rem',
|
||||||
|
padding: '0.35rem 0.75rem',
|
||||||
|
background: 'rgba(14, 165, 233, 0.15)',
|
||||||
|
border: '1px solid rgba(14, 165, 233, 0.5)',
|
||||||
|
borderRadius: '0.3rem',
|
||||||
|
color: '#7DD3FC',
|
||||||
|
fontSize: '0.7rem', fontWeight: '600', fontFamily: 'monospace',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.12s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(14, 165, 233, 0.3)'; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(14, 165, 233, 0.15)'; }}
|
||||||
|
>
|
||||||
|
<ExternalLink style={{ width: 12, height: 12 }} />
|
||||||
|
Open in CARD (ID copied)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { X, Loader, AlertCircle, CheckCircle, XCircle, ArrowRightLeft } from 'lucide-react';
|
import { X, Loader, AlertCircle, CheckCircle, XCircle, ArrowRightLeft, ExternalLink } from 'lucide-react';
|
||||||
|
|
||||||
// ⚠️ CONVENTION: Prefer using REACT_APP_API_BASE without an absolute URL fallback — other components use relative paths via the env var (e.g. '' default) rather than hardcoding http://localhost:3001/api
|
// ⚠️ CONVENTION: Prefer using REACT_APP_API_BASE without an absolute URL fallback — other components use relative paths via the env var (e.g. '' default) rather than hardcoding http://localhost:3001/api
|
||||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
@@ -338,9 +338,52 @@ export default function CardDetailModal({ isOpen, onClose, ip, ownerData: initia
|
|||||||
|
|
||||||
{/* Execution error */}
|
{/* Execution error */}
|
||||||
{execError && (
|
{execError && (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0.75rem', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.3)', borderRadius: '0.375rem', marginBottom: '0.75rem' }}>
|
<div style={{ padding: '0.5rem 0.75rem', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.3)', borderRadius: '0.375rem', marginBottom: '0.75rem' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
<AlertCircle style={{ width: '13px', height: '13px', color: '#EF4444', flexShrink: 0 }} />
|
<AlertCircle style={{ width: '13px', height: '13px', color: '#EF4444', flexShrink: 0 }} />
|
||||||
<span style={{ fontSize: '0.7rem', color: '#FCA5A5' }}>{execError}</span>
|
<span style={{ fontSize: '0.7rem', color: '#FCA5A5' }}>
|
||||||
|
{execError.includes('update_token') ? 'Cannot action via API — this asset has no update token.' : execError}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{execError.includes('update_token') && (
|
||||||
|
<div style={{ marginTop: '0.4rem', paddingTop: '0.4rem', borderTop: '1px solid rgba(239, 68, 68, 0.2)' }}>
|
||||||
|
<span style={{ fontSize: '0.65rem', color: '#94A3B8', display: 'block', marginBottom: '0.3rem' }}>
|
||||||
|
Action this asset directly in CARD instead:
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const hostId = ownerData?.asset_id ? ownerData.asset_id.replace(/-[A-Z]+$/i, '') : ip;
|
||||||
|
try {
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = String(hostId);
|
||||||
|
ta.style.position = 'fixed';
|
||||||
|
ta.style.opacity = '0';
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
} catch (_) { /* best effort */ }
|
||||||
|
window.open('https://card.charter.com/ipn-search', '_blank');
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
display: 'inline-flex', alignItems: 'center', gap: '0.35rem',
|
||||||
|
padding: '0.35rem 0.75rem',
|
||||||
|
background: 'rgba(14, 165, 233, 0.15)',
|
||||||
|
border: '1px solid rgba(14, 165, 233, 0.5)',
|
||||||
|
borderRadius: '0.3rem',
|
||||||
|
color: '#7DD3FC',
|
||||||
|
fontSize: '0.7rem', fontWeight: '600', fontFamily: 'monospace',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.12s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(14, 165, 233, 0.3)'; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(14, 165, 233, 0.15)'; }}
|
||||||
|
>
|
||||||
|
<ExternalLink style={{ width: 12, height: 12 }} />
|
||||||
|
Open in CARD (ID copied)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user