Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
123 changes: 123 additions & 0 deletions src/app/_components/ColorCodedDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
'use client';

import { useState, useRef, useEffect } from 'react';
import type { Server } from '../../types/server';

interface ColorCodedDropdownProps {
servers: Server[];
selectedServer: Server | null;
onServerSelect: (server: Server | null) => void;
placeholder?: string;
disabled?: boolean;
}

export function ColorCodedDropdown({
servers,
selectedServer,
onServerSelect,
placeholder = "Select a server...",
disabled = false
}: ColorCodedDropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);

// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);

const handleServerClick = (server: Server) => {
onServerSelect(server);
setIsOpen(false);
};

const handleClearSelection = () => {
onServerSelect(null);
setIsOpen(false);
};

return (
<div className="relative" ref={dropdownRef}>
{/* Dropdown Button */}
<button
type="button"
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
className={`w-full px-3 py-2 border border-input rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary bg-background text-foreground text-left flex items-center justify-between ${
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-accent'
}`}
>
<span className="truncate">
{selectedServer ? (
<span className="flex items-center gap-2">
{selectedServer.color && (
<span
className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: selectedServer.color }}
/>
)}
{selectedServer.name} ({selectedServer.ip}) - {selectedServer.user}
</span>
) : (
placeholder
)}
</span>
<svg
className={`w-4 h-4 flex-shrink-0 transition-transform ${isOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>

{/* Dropdown Menu */}
{isOpen && (
<div className="absolute z-50 w-full mt-1 bg-card border border-border rounded-md shadow-lg max-h-60 overflow-auto">
{/* Clear Selection Option */}
<button
type="button"
onClick={handleClearSelection}
className="w-full px-3 py-2 text-left text-sm text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
>
{placeholder}
</button>

{/* Server Options */}
{servers.map((server) => (
<button
key={server.id}
type="button"
onClick={() => handleServerClick(server)}
className={`w-full px-3 py-2 text-left text-sm transition-colors flex items-center gap-2 ${
selectedServer?.id === server.id
? 'bg-accent text-accent-foreground'
: 'text-foreground hover:bg-accent hover:text-foreground'
}`}
>
{server.color && (
<span
className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: server.color }}
/>
)}
<span className="truncate">
{server.name} ({server.ip}) - {server.user}
</span>
</button>
))}
</div>
)}
</div>
);
}
31 changes: 14 additions & 17 deletions src/app/_components/ExecutionModeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import { useState, useEffect } from 'react';
import type { Server } from '../../types/server';
import { Button } from './ui/button';
import { ColorCodedDropdown } from './ColorCodedDropdown';
import { SettingsModal } from './SettingsModal';


interface ExecutionModeModalProps {
isOpen: boolean;
onClose: () => void;
Expand Down Expand Up @@ -70,6 +72,12 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
onClose();
};


const handleServerSelect = (server: Server | null) => {
setSelectedServer(server);
};


if (!isOpen) return null;

return (
Expand Down Expand Up @@ -138,23 +146,12 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
</Button>
</div>
) : (
<select
id="server"
value={selectedServer?.id ?? ''}
onChange={(e) => {
const serverId = parseInt(e.target.value);
const server = servers.find(s => s.id === serverId);
setSelectedServer(server ?? null);
}}
className="w-full px-3 py-2 border border-input rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary bg-background text-foreground"
>
<option value="">Select a server...</option>
{servers.map((server) => (
<option key={server.id} value={server.id}>
{server.name} ({server.ip}) - {server.user}
</option>
))}
</select>
<ColorCodedDropdown
servers={servers}
selectedServer={selectedServer}
onServerSelect={handleServerSelect}
placeholder="Select a server..."
/>
)}
</div>

Expand Down
49 changes: 49 additions & 0 deletions src/app/_components/GeneralSettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
const [githubToken, setGithubToken] = useState('');
const [saveFilter, setSaveFilter] = useState(false);
const [savedFilters, setSavedFilters] = useState<any>(null);
const [colorCodingEnabled, setColorCodingEnabled] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
Expand All @@ -35,6 +36,7 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
void loadSaveFilter();
void loadSavedFilters();
void loadAuthCredentials();
void loadColorCodingSetting();
}
}, [isOpen]);

Expand Down Expand Up @@ -148,6 +150,43 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
}
};

const loadColorCodingSetting = async () => {
try {
const response = await fetch('/api/settings/color-coding');
if (response.ok) {
const data = await response.json();
setColorCodingEnabled(Boolean(data.enabled));
}
} catch (error) {
console.error('Error loading color coding setting:', error);
}
};

const saveColorCodingSetting = async (enabled: boolean) => {
try {
const response = await fetch('/api/settings/color-coding', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ enabled }),
});

if (response.ok) {
setColorCodingEnabled(enabled);
setMessage({ type: 'success', text: 'Color coding setting saved successfully' });
setTimeout(() => setMessage(null), 3000);
} else {
setMessage({ type: 'error', text: 'Failed to save color coding setting' });
setTimeout(() => setMessage(null), 3000);
}
} catch (error) {
console.error('Error saving color coding setting:', error);
setMessage({ type: 'error', text: 'Failed to save color coding setting' });
setTimeout(() => setMessage(null), 3000);
}
};

const loadAuthCredentials = async () => {
setAuthLoading(true);
try {
Expand Down Expand Up @@ -345,6 +384,16 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
</div>
)}
</div>

<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Server Color Coding</h4>
<p className="text-sm text-muted-foreground mb-4">Enable color coding for servers to visually distinguish them throughout the application.</p>
<Toggle
checked={colorCodingEnabled}
onCheckedChange={saveColorCodingSetting}
label="Enable server color coding"
/>
</div>
</div>
</div>
)}
Expand Down
18 changes: 15 additions & 3 deletions src/app/_components/InstalledScriptsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Terminal } from './Terminal';
import { StatusBadge } from './Badge';
import { Button } from './ui/button';
import { ScriptInstallationCard } from './ScriptInstallationCard';
import { getContrastColor } from '../../lib/colorUtils';

interface InstalledScript {
id: number;
Expand All @@ -17,6 +18,7 @@ interface InstalledScript {
server_ip: string | null;
server_user: string | null;
server_password: string | null;
server_color: string | null;
installation_date: string;
status: 'in_progress' | 'success' | 'failed';
output_log: string | null;
Expand Down Expand Up @@ -773,7 +775,11 @@ export function InstalledScriptsTab() {
</thead>
<tbody className="bg-card divide-y divide-gray-200">
{filteredScripts.map((script) => (
<tr key={script.id} className="hover:bg-accent">
<tr
key={script.id}
className="hover:bg-accent"
style={{ borderLeft: `4px solid ${script.server_color ?? 'transparent'}` }}
>
<td className="px-6 py-4 whitespace-nowrap">
{editingScriptId === script.id ? (
<div className="space-y-2">
Expand Down Expand Up @@ -811,8 +817,14 @@ export function InstalledScriptsTab() {
)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="text-sm text-muted-foreground">
{script.server_name ?? 'Local'}
<span
className="text-sm px-3 py-1 rounded"
style={{
backgroundColor: script.server_color ?? 'transparent',
color: script.server_color ? getContrastColor(script.server_color) : 'inherit'
}}
>
{script.server_name ?? '-'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
Expand Down
19 changes: 15 additions & 4 deletions src/app/_components/ScriptInstallationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { Button } from './ui/button';
import { StatusBadge } from './Badge';
import { getContrastColor } from '../../lib/colorUtils';

interface InstalledScript {
id: number;
Expand All @@ -13,6 +14,7 @@ interface InstalledScript {
server_ip: string | null;
server_user: string | null;
server_password: string | null;
server_color: string | null;
installation_date: string;
status: 'in_progress' | 'success' | 'failed';
output_log: string | null;
Expand Down Expand Up @@ -50,7 +52,10 @@ export function ScriptInstallationCard({
};

return (
<div className="bg-card border border-border rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow">
<div
className="bg-card border border-border rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow"
style={{ borderLeft: `4px solid ${script.server_color ?? 'transparent'}` }}
>
{/* Header with Script Name and Status */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1 min-w-0">
Expand Down Expand Up @@ -102,9 +107,15 @@ export function ScriptInstallationCard({
{/* Server */}
<div>
<div className="text-xs font-medium text-muted-foreground mb-1">Server</div>
<div className="text-sm text-muted-foreground">
{script.server_name ?? 'Local'}
</div>
<span
className="text-sm px-3 py-1 rounded inline-block"
style={{
backgroundColor: script.server_color ?? 'transparent',
color: script.server_color ? getContrastColor(script.server_color) : 'inherit'
}}
>
{script.server_name ?? '-'}
</span>
</div>

{/* Installation Date */}
Expand Down
Loading