@mcp-shark/mcp-shark
Version:
Aggregate multiple Model Context Protocol (MCP) servers into a single unified interface with a powerful monitoring UI. Prov deep visibility into every request and response.
291 lines (284 loc) • 8.67 kB
JSX
import { useState, useRef } from 'react';
import { colors, fonts } from '../../theme';
import { CheckIcon, LoadingSpinner, CacheIcon } from '../SmartScanIcons';
import { ExternalLinkIcon } from '../SmartScanIcons';
import { IconTrash } from '@tabler/icons-react';
import ConfirmationModal from '../ConfirmationModal';
export default function SmartScanControls({
apiToken,
setApiToken,
saveToken,
loadingData,
discoverMcpData,
discoveredServers,
selectedServers,
setSelectedServers,
runScan,
scanning,
clearCache,
clearingCache,
}) {
const saveTokenTimeoutRef = useRef(null);
const [showClearCacheModal, setShowClearCacheModal] = useState(false);
const handleTokenChange = (newToken) => {
setApiToken(newToken);
if (saveTokenTimeoutRef.current) {
clearTimeout(saveTokenTimeoutRef.current);
}
if (newToken) {
saveTokenTimeoutRef.current = setTimeout(() => {
saveToken(newToken);
}, 1000);
} else {
saveToken('');
}
};
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '20px',
flexWrap: 'wrap',
flex: 1,
justifyContent: 'flex-end',
}}
>
{/* API Token Section */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<label
style={{
fontSize: '12px',
fontWeight: '600',
color: colors.textSecondary,
fontFamily: fonts.body,
whiteSpace: 'nowrap',
}}
>
API Token:
</label>
<div style={{ position: 'relative', width: '200px' }}>
<input
type="password"
value={apiToken}
onChange={(e) => handleTokenChange(e.target.value)}
placeholder="sk_..."
style={{
width: '100%',
padding: '8px 10px',
paddingRight: apiToken ? '28px' : '10px',
border: `1px solid ${apiToken ? colors.accentGreen : colors.borderMedium}`,
borderRadius: '8px',
fontSize: '12px',
fontFamily: fonts.body,
background: colors.bgCard,
color: colors.textPrimary,
boxSizing: 'border-box',
transition: 'all 0.2s ease',
}}
onFocus={(e) => {
e.currentTarget.style.borderColor = colors.accentBlue;
e.currentTarget.style.boxShadow = `0 0 0 2px ${colors.accentBlue}20`;
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = apiToken
? colors.accentGreen
: colors.borderMedium;
e.currentTarget.style.boxShadow = 'none';
}}
/>
{apiToken && (
<div
style={{
position: 'absolute',
right: '8px',
top: '50%',
transform: 'translateY(-50%)',
}}
>
<CheckIcon size={12} color={colors.accentGreen} />
</div>
)}
</div>
<a
href="https://smart.mcpshark.sh/tokens"
target="_blank"
rel="noopener noreferrer"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
fontSize: '11px',
color: colors.accentBlue,
textDecoration: 'none',
fontFamily: fonts.body,
fontWeight: '500',
whiteSpace: 'nowrap',
}}
onMouseEnter={(e) => {
e.currentTarget.style.textDecoration = 'underline';
}}
onMouseLeave={(e) => {
e.currentTarget.style.textDecoration = 'none';
}}
>
<span>Get token</span>
<ExternalLinkIcon size={10} color={colors.accentBlue} />
</a>
</div>
{/* Discover MCP Data Section */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<label
style={{
fontSize: '12px',
fontWeight: '600',
color: colors.textSecondary,
fontFamily: fonts.body,
whiteSpace: 'nowrap',
}}
>
Servers:
</label>
<button
onClick={discoverMcpData}
disabled={loadingData}
style={{
padding: '8px 14px',
background: !loadingData ? colors.buttonPrimary : colors.buttonSecondary,
color: !loadingData ? colors.textInverse : colors.textTertiary,
border: 'none',
borderRadius: '6px',
fontSize: '12px',
fontWeight: '600',
fontFamily: fonts.body,
cursor: !loadingData ? 'pointer' : 'not-allowed',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
gap: '6px',
whiteSpace: 'nowrap',
}}
onMouseEnter={(e) => {
if (!loadingData) {
e.currentTarget.style.background = colors.buttonPrimaryHover;
e.currentTarget.style.transform = 'translateY(-1px)';
}
}}
onMouseLeave={(e) => {
if (!loadingData) {
e.currentTarget.style.background = colors.buttonPrimary;
e.currentTarget.style.transform = 'translateY(0)';
}
}}
>
{loadingData ? (
<>
<LoadingSpinner size={12} />
<span>Discovering...</span>
</>
) : (
<>
<CheckIcon size={12} color={colors.textInverse} />
<span>Discover</span>
</>
)}
</button>
{discoveredServers.length > 0 && (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '6px 10px',
background: colors.bgTertiary,
borderRadius: '8px',
fontSize: '11px',
fontWeight: '600',
color: colors.textPrimary,
fontFamily: fonts.body,
}}
>
<CheckIcon size={12} color={colors.accentGreen} />
<span>
{discoveredServers.length} server{discoveredServers.length !== 1 ? 's' : ''}
</span>
</div>
)}
</div>
{/* Clear Cache Button */}
<button
onClick={() => setShowClearCacheModal(true)}
disabled={clearingCache}
style={{
padding: '8px 14px',
background: colors.buttonSecondary,
border: `1px solid ${colors.borderLight}`,
color: colors.textSecondary,
cursor: clearingCache ? 'not-allowed' : 'pointer',
fontSize: '12px',
fontWeight: '500',
fontFamily: fonts.body,
borderRadius: '8px',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
gap: '6px',
whiteSpace: 'nowrap',
opacity: clearingCache ? 0.5 : 1,
}}
onMouseEnter={(e) => {
if (!clearingCache) {
e.currentTarget.style.background = colors.buttonSecondaryHover;
e.currentTarget.style.color = colors.textPrimary;
}
}}
onMouseLeave={(e) => {
if (!clearingCache) {
e.currentTarget.style.background = colors.buttonSecondary;
e.currentTarget.style.color = colors.textSecondary;
}
}}
title="Clear cached scan results"
>
{clearingCache ? (
<>
<LoadingSpinner size={12} />
<span>Clearing...</span>
</>
) : (
<>
<IconTrash size={14} stroke={1.5} />
<span>Clear Cache</span>
</>
)}
</button>
<ConfirmationModal
isOpen={showClearCacheModal}
onClose={() => setShowClearCacheModal(false)}
onConfirm={async () => {
const result = await clearCache();
if (result.success) {
setShowClearCacheModal(false);
}
}}
title="Clear Scan Cache?"
message="Are you sure you want to clear all cached scan results? This will force fresh scans for all servers on the next scan."
confirmText="Clear Cache"
cancelText="Cancel"
danger={false}
/>
</div>
);
}