Skip to content
Merged
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
9 changes: 6 additions & 3 deletions frontend/src/components/forms/IconPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import { useState, useMemo, useRef, useEffect } from 'react';
import {
Server, Cloud, Shield, Lock, Globe, Network, Monitor, Laptop,
HardDrive, Database, Terminal, Bug, Cog, Wrench, Cpu, Radar,
Router, Flame, Bolt, Rocket, Satellite, Building,
Router, Flame, Bolt, Rocket, Satellite, Building,
Warehouse, Factory, Store, School, Hospital, Fingerprint,
Key, Eye, Search, ChartLine, ChartBar, Bell, Flag, Star,
Heart, Leaf, Anchor, Plane, Drone, type LucideIcon,
Heart, Leaf, Anchor, Plane, Drone, Tag, type LucideIcon,
} from 'lucide-react';
import { cn } from '$/lib/cn';

const PRESET_ICONS: { name: string; Icon: LucideIcon }[] = [
{ name: 'tag', Icon: Tag },
{ name: 'server', Icon: Server },
{ name: 'cloud', Icon: Cloud },
{ name: 'shield', Icon: Shield },
Expand All @@ -34,7 +35,6 @@ const PRESET_ICONS: { name: string; Icon: LucideIcon }[] = [
{ name: 'cpu', Icon: Cpu },
{ name: 'radar', Icon: Radar },
{ name: 'router', Icon: Router },
{ name: 'network', Icon: Network }, // deduped below — remove
{ name: 'flame', Icon: Flame },
{ name: 'bolt', Icon: Bolt },
{ name: 'rocket', Icon: Rocket },
Expand Down Expand Up @@ -74,6 +74,9 @@ export function resolveEnvIcon(icon: string | undefined | null): LucideIcon | nu
return ICON_MAP[name] ?? null;
}

/** Alias for resolveEnvIcon — tags use the same icon set. */
export const resolveTagIcon = resolveEnvIcon;

interface IconPickerProps {
value: string;
onChange: (icon: string) => void;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/features/environments/EnvConfigPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function makeEnv(overrides: Partial<TLSEnvironment> = {}): TLSEnvironment {
msi_package: '',
pkg_package: '',
debug_http: false,
icon: 'fas fa-wrench',
icon: 'wrench',
options: '{}',
schedule: '{}',
packs: '{}',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function makeEnv(overrides: Partial<TLSEnvironment> = {}): TLSEnvironment {
msi_package: '',
pkg_package: '',
debug_http: false,
icon: 'fas fa-wrench',
icon: 'wrench',
options: '{}',
schedule: '{}',
packs: '{}',
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/features/nodes/NodeDetailPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ function makeTags(): AdminTag[] {
name: 'prod',
description: 'Production',
color: '#2ecc71',
icon: 'fas fa-tag',
icon: 'tag',
created_by: 'alice',
custom_tag: 'tag',
auto_tag: false,
Expand Down Expand Up @@ -553,7 +553,7 @@ describe('NodeDetailPage', () => {
name: 'prod',
description: 'Production',
color: '#2ecc71',
icon: 'fas fa-tag',
icon: 'tag',
created_by: 'alice',
custom_tag: 'tag',
auto_tag: false,
Expand All @@ -568,7 +568,7 @@ describe('NodeDetailPage', () => {
name: 'critical',
description: 'Critical',
color: '#e74c3c',
icon: 'fas fa-tag',
icon: 'tag',
created_by: 'alice',
custom_tag: 'tag',
auto_tag: false,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/features/nodes/NodesTablePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ describe('NodesTablePage', () => {
name: 'prod',
description: 'Production',
color: '#2ecc71',
icon: 'fas fa-tag',
icon: 'tag',
created_by: 'alice',
custom_tag: 'tag',
auto_tag: false,
Expand All @@ -468,7 +468,7 @@ describe('NodesTablePage', () => {
name: 'critical',
description: 'Critical',
color: '#e74c3c',
icon: 'fas fa-tag',
icon: 'tag',
created_by: 'alice',
custom_tag: 'tag',
auto_tag: false,
Expand Down
33 changes: 19 additions & 14 deletions frontend/src/features/nodes/nodeSignals.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AdminTag, NodeHealth, NodeHealthStatus } from '$/api/types';
import { cn } from '$/lib/cn';
import { resolveTagIcon } from '$/components/forms/IconPicker';

const HEALTH_LABELS: Record<NodeHealthStatus, string> = {
healthy: 'healthy',
Expand Down Expand Up @@ -57,20 +58,24 @@ export function TagChips({
const overflow = list.length - visible.length;
return (
<div className="flex flex-wrap items-center gap-1">
{visible.map((tag) => (
<span
key={`${tag.id}-${tag.name}`}
className="inline-flex max-w-[120px] items-center rounded-full border px-1.5 py-0.5 text-[10.5px] font-mono-tabular leading-tight"
style={{
borderColor: `${tag.color || '#64748b'}55`,
backgroundColor: `${tag.color || '#64748b'}18`,
color: tag.color || 'var(--text-2)',
}}
title={tag.description || tag.name}
>
<span className="truncate">{tag.name}</span>
</span>
))}
{visible.map((tag) => {
const IconComp = resolveTagIcon(tag.icon);
return (
<span
key={`${tag.id}-${tag.name}`}
className="inline-flex max-w-[120px] items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10.5px] font-mono-tabular leading-tight"
style={{
borderColor: `${tag.color || '#64748b'}55`,
backgroundColor: `${tag.color || '#64748b'}18`,
color: tag.color || 'var(--text-2)',
}}
title={tag.description || tag.name}
>
{IconComp && <IconComp className="w-2.5 h-2.5 flex-shrink-0" aria-hidden />}
<span className="truncate">{tag.name}</span>
</span>
);
})}
{overflow > 0 && (
<span className="text-[10.5px] font-mono-tabular text-[color:var(--text-3)]">
+{overflow}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/features/tags/TagsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function makeTag(overrides: Partial<AdminTag> = {}): AdminTag {
name: 'production',
description: 'Production environment',
color: '#5b8def',
icon: 'fas fa-server',
icon: 'server',
created_by: 'admin',
custom_tag: 'tag',
auto_tag: false,
Expand Down
24 changes: 9 additions & 15 deletions frontend/src/features/tags/TagsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { cn } from '$/lib/cn';
import { SkeletonRow } from '$/components/data/Skeleton';
import { EmptyState } from '$/components/data/EmptyState';
import { ModalShell } from '$/components/feedback/ModalShell';
import { IconPicker, resolveTagIcon } from '$/components/forms/IconPicker';

type ModalMode =
| { kind: 'closed' }
Expand All @@ -18,7 +19,7 @@ type ModalMode =
| { kind: 'delete'; tag: AdminTag };

const DEFAULT_COLOR = '#5b8def';
const DEFAULT_ICON = 'fas fa-tag';
const DEFAULT_ICON = 'tag';
const TAG_TYPE_REGULAR = 6; // mirrors pkg/tags.TagTypeTag

export function TagsPage() {
Expand Down Expand Up @@ -278,7 +279,10 @@ export function TagsPage() {
color: tag.color || DEFAULT_COLOR,
}}
>
<i className={tag.icon || DEFAULT_ICON} aria-hidden />
{(() => {
const IconComp = resolveTagIcon(tag.icon || DEFAULT_ICON);
return IconComp ? <IconComp className="w-3 h-3" aria-hidden /> : null;
})()}
<span className="font-mono-tabular">{tag.name}</span>
</span>
</td>
Expand Down Expand Up @@ -535,23 +539,13 @@ function TagFormModal({
</div>
<div className="flex-1">
<label htmlFor="tag-icon" className="block text-xs font-semibold text-[color:var(--text-2)] mb-1">
Icon class
Icon
</label>
<input
<IconPicker
id="tag-icon"
type="text"
value={icon}
onChange={(e) => setIcon(e.target.value)}
placeholder={DEFAULT_ICON}
className={cn(
'w-full px-3 py-2 text-sm rounded-md border border-[color:var(--border)]',
'bg-[color:var(--bg-2)] text-[color:var(--text-1)] font-mono-tabular',
'focus:outline focus:outline-2 focus:outline-[color:var(--signal)]',
)}
onChange={setIcon}
/>
<p className="mt-1 text-[10px] text-[color:var(--text-3)]">
Font Awesome class (e.g. <code>fas fa-server</code>).
</p>
</div>
</div>

Expand Down
2 changes: 1 addition & 1 deletion pkg/environments/environments.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const (
// DefaultCarverBlockPath as default block endpoint for the carver
DefaultCarverBlockPath string = "block"
// DefaultEnvironmentIcon as default icon to use for environments
DefaultEnvironmentIcon string = "fas fa-wrench"
DefaultEnvironmentIcon string = "wrench"
// DefaultEnvironmentType as default type to use for environments
DefaultEnvironmentType string = "osquery"
// DefaultSecretLength as default length for secrets
Expand Down
7 changes: 5 additions & 2 deletions pkg/tags/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

const (
// DefaultTagIcon as default icon to use for tags
DefaultTagIcon string = "fas fa-tag"
DefaultTagIcon string = "tag"
// DefaultAutoTagUser as default user ID to be used for auto tagging
DefaultAutoTagUser uint = 0
// DefaultAutocreated as default username and description for tags
Expand Down Expand Up @@ -435,7 +435,10 @@ func (m *TagManager) TagNode(name string, node nodes.OsqueryNode, user string, a
tag = newTag
}
if m.IsTagged(tag.Name, node) {
return fmt.Errorf("node already tagged")
// Idempotent: tagging an already-tagged node is not an error.
// This makes batch-tagging safe — the frontend can tag all selected
// nodes without checking which are already tagged.
return nil
}
tagged := TaggedNode{
Tag: tag.Name,
Expand Down
Loading