Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useMemo } from "react"
import styled from "styled-components"
import { MdCheckCircle as Checkmark } from "react-icons/md"
import { MdCheckCircle as Checkmark, MdClose as CloseIcon } from "react-icons/md"

import {
getIcon,
Expand All @@ -10,13 +10,18 @@ import {
} from "../../util/connectionHelpers"
import { Connection } from "../../contexts/Standalone/useStandalone"

const Container = styled.div`
interface ContainerProps {
$isConnected: boolean
}

const Container = styled.div<ContainerProps>`
display: flex;
flex-direction: row;
flex: 0 0 auto;
cursor: pointer;
margin-right: 20px;
align-items: center;
opacity: ${(props) => (props.$isConnected ? 1 : 0.5)};
`

const IconContainer = styled.div`
Expand All @@ -30,22 +35,57 @@ const CheckmarkContainer = styled.div`
color: green;
`

interface StatusDotProps {
$isConnected: boolean
}

const StatusDot = styled.div<StatusDotProps>`
position: absolute;
top: -2px;
left: -2px;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: ${(props) => (props.$isConnected ? "#50c878" : "#ff6b6b")};
border: 2px solid ${(props) => props.theme.subtleLine};
`

const InfoContainer = styled.div`
margin-left: 10px;
`

const DeleteButton = styled.div`
margin-left: 8px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #ff6b6b;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;

&:hover {
background-color: #ff4444;
}
`

interface ConnectionSelectorProps {
selectedConnection: Connection | null
connection: Connection
onClick: () => void
onDelete?: (clientId: string) => void
}

export default function ConnectionSelector({
selectedConnection,
connection,
onClick,
onDelete,
}: ConnectionSelectorProps) {
const isSelected = selectedConnection && selectedConnection.clientId === connection.clientId
const isConnected = connection.connected

const [ConnectionIcon, platformName, platformDetails, connectionName] = useMemo(() => {
return [
Expand All @@ -56,10 +96,18 @@ export default function ConnectionSelector({
]
}, [connection])

const handleDelete = (e: React.MouseEvent) => {
e.stopPropagation()
if (onDelete && !isConnected) {
onDelete(connection.clientId)
}
}

return (
<Container onClick={onClick}>
<Container onClick={onClick} $isConnected={isConnected}>
<IconContainer>
<ConnectionIcon size={32} />
<StatusDot $isConnected={isConnected} title={isConnected ? "Connected" : "Disconnected"} />
{isSelected && (
<CheckmarkContainer>
<Checkmark />
Expand All @@ -72,6 +120,11 @@ export default function ConnectionSelector({
{platformName} {platformDetails}
</div>
</InfoContainer>
{!isConnected && onDelete && (
<DeleteButton onClick={handleDelete} title="Remove disconnected connection">
<CloseIcon size={12} color="white" />
</DeleteButton>
)}
</Container>
)
}
19 changes: 15 additions & 4 deletions apps/reactotron-app/src/renderer/components/Footer/Stateless.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ function renderExpanded(
serverStatus: ServerStatus,
connections: Connection[],
selectedConnection: Connection | null,
onChangeConnection: (clientId: string | null) => void
onChangeConnection: (clientId: string | null) => void,
onDeleteConnection?: (clientId: string) => void
) {
return (
<ConnectionContainer>
Expand All @@ -92,6 +93,7 @@ function renderExpanded(
selectedConnection={selectedConnection}
connection={c}
onClick={() => onChangeConnection(c.clientId)}
onDelete={onDeleteConnection}
/>
))}
</ConnectionContainer>
Expand All @@ -111,10 +113,12 @@ function renderCollapsed(
connections: Connection[],
selectedConnection: Connection | null
) {
const activeConnections = connections.filter((c) => c.connected).length
const totalConnections = connections.length
return (
<>
<ConnectionInfo>
port {config.get("serverPort")} | {connections.length} connections
port {config.get("serverPort")} | {activeConnections}/{totalConnections} connections
</ConnectionInfo>
{serverStatus === "portUnavailable" && (
<ConnectionInfo>Port 9090 unavailable.</ConnectionInfo>
Expand All @@ -134,6 +138,7 @@ interface Props {
isOpen: boolean
setIsOpen: (isOpen: boolean) => void
onChangeConnection: (clientId: string | null) => void
onDeleteConnection?: (clientId: string) => void
mcpStatus: McpStatus
mcpPort: number | null
onToggleMcp: () => void
Expand All @@ -146,16 +151,22 @@ function Header({
isOpen,
setIsOpen,
onChangeConnection,
onDeleteConnection,
mcpStatus,
mcpPort,
onToggleMcp,
}: Props) {
const renderMethod = isOpen ? renderExpanded : renderCollapsed
const renderContent = () => {
if (isOpen) {
return renderExpanded(serverStatus, connections, selectedConnection, onChangeConnection, onDeleteConnection)
}
return renderCollapsed(serverStatus, connections, selectedConnection)
}

return (
<Container>
<ContentContainer onClick={() => !isOpen && setIsOpen(true)} $isOpen={isOpen}>
{renderMethod(serverStatus, connections, selectedConnection, onChangeConnection)}
{renderContent()}
<McpButton
$active={mcpStatus === "started"}
onClick={(e) => { e.stopPropagation(); onToggleMcp() }}
Expand Down
3 changes: 2 additions & 1 deletion apps/reactotron-app/src/renderer/components/Footer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import StandaloneContext from "../../contexts/Standalone"
import Footer from "./Stateless"

export default function ConnectedFooter() {
const { serverStatus, connections, selectedConnection, selectConnection, mcpStatus, mcpPort, toggleMcp } =
const { serverStatus, connections, selectedConnection, selectConnection, deleteConnection, mcpStatus, mcpPort, toggleMcp } =
useContext(StandaloneContext)
const [isOpen, setIsOpen] = useState(false)

Expand All @@ -15,6 +15,7 @@ export default function ConnectedFooter() {
connections={connections}
selectedConnection={selectedConnection}
onChangeConnection={selectConnection}
onDeleteConnection={deleteConnection}
isOpen={isOpen}
setIsOpen={setIsOpen}
mcpStatus={mcpStatus}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface Context {
connections: Connection[]
selectedConnection: Connection
selectConnection: (clientId: string) => void
deleteConnection: (clientId: string) => void
mcpStatus: McpStatus
mcpPort: number | null
toggleMcp: () => void
Expand All @@ -25,6 +26,7 @@ const StandaloneContext = React.createContext<Context>({
connections: [],
selectedConnection: null,
selectConnection: null,
deleteConnection: () => {},
mcpStatus: "stopped",
mcpPort: null,
toggleMcp: () => {},
Expand All @@ -39,6 +41,7 @@ const Provider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
selectedClientId,
selectedConnection,
selectConnection,
deleteConnection,
clearSelectedConnectionCommands,
serverStarted,
serverStopped,
Expand Down Expand Up @@ -130,6 +133,7 @@ const Provider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
connections,
selectedConnection,
selectConnection,
deleteConnection,
mcpStatus,
mcpPort,
toggleMcp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum ActionTypes {
ServerStopped = "SERVER_STOPPED",
AddConnection = "ADD_CONNECTION",
RemoveConnection = "REMOVE_CONNECTION",
DeleteConnection = "DELETE_CONNECTION",
ClearConnectionCommands = "CLEAR_CONNECTION_COMMANDS",
CommandReceived = "COMMAND_RECEIVED",
ChangeSelectedClientId = "CHANGE_SELECTED_CLIENT_ID",
Expand Down Expand Up @@ -49,6 +50,7 @@ type Action =
type: ActionTypes.AddConnection | ActionTypes.RemoveConnection
payload: ReactotronConnection
}
| { type: ActionTypes.DeleteConnection; payload: string }
| { type: ActionTypes.ChangeSelectedClientId; payload: string }
| { type: ActionTypes.CommandReceived; payload: any } // TODO: Type this better!
| { type: ActionTypes.ClearConnectionCommands }
Expand Down Expand Up @@ -128,6 +130,26 @@ export function reducer(state: State, action: Action) {
}
}
})
case ActionTypes.DeleteConnection:
return produce(state, (draftState) => {
const connectionIndex = draftState.connections.findIndex(
(c) => c.clientId === action.payload
)

if (connectionIndex === -1) return

const connection = draftState.connections[connectionIndex]

if (connection.connected) return

draftState.connections.splice(connectionIndex, 1)
if (draftState.selectedClientId === action.payload) {
const remainingConnections = draftState.connections.filter((c) => c.connected)
draftState.selectedClientId = remainingConnections.length > 0
? remainingConnections[0].clientId
: null
}
})
case ActionTypes.CommandReceived:
return produce(state, (draftState) => {
if (!action.payload.clientId) {
Expand Down Expand Up @@ -240,6 +262,10 @@ function useStandalone() {
dispatch({ type: ActionTypes.PortUnavailable, payload: undefined })
}, [])

const deleteConnection = useCallback((clientId: string) => {
dispatch({ type: ActionTypes.DeleteConnection, payload: clientId })
}, [])

return {
...state,
selectedConnection: state.connections.find((c) => c.clientId === state.selectedClientId),
Expand All @@ -252,6 +278,7 @@ function useStandalone() {
clearSelectedConnectionCommands,
addCommandListener,
portUnavailable,
deleteConnection,
}
}

Expand Down