Add fleet groups, agent screenshots, deploy guards, and Crucible polish.
This commit is contained in:
97
server/web/src/components/Fleet/CreateGroupModal.tsx
Normal file
97
server/web/src/components/Fleet/CreateGroupModal.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FLEET_GROUP_COLORS, normalizeGroupColor } from '../../help/fleetGroups';
|
||||
import './CreateGroupModal.css';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
agentCount: number;
|
||||
onClose: () => void;
|
||||
onCreate: (name: string, color: string) => void;
|
||||
}
|
||||
|
||||
export default function CreateGroupModal({ open, agentCount, onClose, onCreate }: Props) {
|
||||
const [name, setName] = useState('');
|
||||
const [color, setColor] = useState<string>(FLEET_GROUP_COLORS[0]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setName('');
|
||||
setColor(FLEET_GROUP_COLORS[groupsColorIndex(agentCount) % FLEET_GROUP_COLORS.length]);
|
||||
}
|
||||
}, [open, agentCount]);
|
||||
|
||||
if (!open || agentCount < 1) return null;
|
||||
|
||||
const submit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return;
|
||||
onCreate(trimmed, normalizeGroupColor(color));
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fleet-group-modal-backdrop" role="presentation" onClick={onClose}>
|
||||
<div
|
||||
className="fleet-group-modal card"
|
||||
role="dialog"
|
||||
aria-labelledby="fleet-group-modal-title"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2 id="fleet-group-modal-title" className="font-display">Create group</h2>
|
||||
<p className="form-hint">
|
||||
Saves {agentCount} selected machine{agentCount === 1 ? '' : 's'} — usable in Fleet Roster and Crucible.
|
||||
</p>
|
||||
<form onSubmit={submit}>
|
||||
<label className="label" htmlFor="fleet-group-name">Group name</label>
|
||||
<input
|
||||
id="fleet-group-name"
|
||||
className="input"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="e.g. Living room PCs"
|
||||
autoFocus
|
||||
maxLength={64}
|
||||
/>
|
||||
|
||||
<p className="label" style={{ marginTop: '1rem' }}>Group color</p>
|
||||
<div className="fleet-group-color-grid">
|
||||
{FLEET_GROUP_COLORS.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
className={`fleet-group-swatch ${color === c ? 'selected' : ''}`}
|
||||
style={{ background: c }}
|
||||
title={c}
|
||||
aria-label={`Color ${c}`}
|
||||
onClick={() => setColor(c)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="fleet-group-custom-color">
|
||||
<input
|
||||
type="color"
|
||||
value={normalizeGroupColor(color)}
|
||||
onChange={(e) => setColor(e.target.value)}
|
||||
aria-label="Custom color"
|
||||
/>
|
||||
<span className="font-tech">{normalizeGroupColor(color)}</span>
|
||||
</div>
|
||||
|
||||
<div className="fleet-group-modal-actions">
|
||||
<button type="button" className="btn btn-outline" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={!name.trim()}>
|
||||
Create group
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function groupsColorIndex(n: number): number {
|
||||
return Math.abs(n) % FLEET_GROUP_COLORS.length;
|
||||
}
|
||||
Reference in New Issue
Block a user