Skip to content

feat(hosts): reusable color-coded host tags#667

Open
parsa222 wants to merge 1 commit into
PasarGuard:devfrom
parsa222:host-tags
Open

feat(hosts): reusable color-coded host tags#667
parsa222 wants to merge 1 commit into
PasarGuard:devfrom
parsa222:host-tags

Conversation

@parsa222

@parsa222 parsa222 commented Jul 1, 2026

Copy link
Copy Markdown

Summary

Adds reusable, color-coded host tags. You can create a palette of named tags, assign multiple tags to any host, and the host is tinted with its first tag's color (a soft, glassy fill) with one chip shown per tag. Tags are managed inline from the host form (create / edit / delete) and render consistently across the grid, list, and table host views.

Motivation: on a large fleet, hosts are hard to scan visually. Tags give a lightweight, user-defined way to group and color hosts (by region, provider, purpose, etc.) without adding rigid schema.

What's included

Backend

  • HostTag model + hosts_tags_association many-to-many table (Alembic migration a821c186b426).
  • Tag CRUD in the operation layer and a router at /api/host/tags (GET/POST) and /api/host/tags/{tag_id} (PUT/DELETE), gated by the existing hosts RBAC resource.
  • tag_ids accepted on host create/modify; tags returned on host responses.
  • Tags are ordered by id for a deterministic tint color. Bulk host delete clears tag links explicitly (SQLite has no FK cascade). Duplicate tag names return 409.

Frontend

  • Theme-aware color palette and a tag picker (assign + inline create/edit/delete) in the host form.
  • Card/row tinting applied across grid, list, and table views, guarded so it does not fight the selected-row highlight.
  • React Query hooks for the new endpoints.

Tests

  • Unit tests for schema validation.
  • API/e2e tests: tag CRUD, delete-tag cascade, dedup of repeated tag_ids, invalid tag id, host clone, host reorder, and bulk delete of a tagged host.

Database compatibility

Verified end to end on SQLite, MySQL / MariaDB, and PostgreSQL / TimescaleDB.

Notes for reviewers

  • Generated API client: dashboard/src/service/api/index.ts is Orval-generated, but bun run gen:api currently crashes on Node >= 22 (bundled Spectral/ajv throws at import). The few new types were hand-added to match what a regen would produce; they are self-healing on the next real regen on Node <= 20 / in CI.
  • No UI em dashes or emojis in any user-facing string, per project convention.

Checklist

  • ruff check . passes (lint clean)
  • ruff format clean on all changed files
  • Rebased onto dev; migration chains after dev's head (b6c9d0e1f2a3), so alembic heads shows a single head
  • Full migration chain (dev's migrations + this one) applies on SQLite and PostgreSQL; migration content also verified on MySQL/MariaDB
  • Full test suite passes (406 tests) on the rebased branch
  • Backend logic lives in the operation layer

Summary by CodeRabbit

  • New Features

    • Added host tags throughout the app, including tag creation, editing, deletion, and assignment to hosts.
    • Hosts can now display tag chips and tag colors in lists, cards, and the host editor.
    • Added support for selecting multiple tags when creating or updating a host.
  • Bug Fixes

    • Improved host/tag consistency so tag relationships are preserved during cloning, bulk actions, and deletions.
    • Prevented duplicate tag entries and handled invalid or missing tag selections more gracefully.

Create reusable, color-coded tags and assign multiple to each host; the host card is tinted with the first tag's color (a soft, glassy fill) and shows one chip per tag.

Backend: HostTag model + hosts_tags_association M2M (migration a821c186b426), tag CRUD/operation/router at /api/host/tags (RBAC via the hosts resource), and tag_ids/tags on host create/modify/response. Tags order_by id for a deterministic tint color; bulk host delete clears tag links explicitly (SQLite has no FK cascade).

Frontend: theme-aware palette, react-query hooks, a tag picker (assign + inline create/edit/delete) in the host form, and card/row coloring across grid, list, and table views. The orval client was hand-extended because codegen is broken on Node 22.

Tests: unit (schema validation) + API/e2e (CRUD, delete-tag cascade, dedup, invalid id, clone, reorder, bulk-delete). Verified on SQLite, MySQL/MariaDB, and PostgreSQL/TimescaleDB.
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR introduces host tagging: a new HostTag entity with color and name, database tables and migration, backend CRUD/operation/API layers for tag management, host associations via a join table, and dashboard UI (picker, chips, tinted list rows) integrated into host forms and lists, plus unit and API tests.

Changes

Host Tagging Feature

Layer / File(s) Summary
Database schema and models
app/db/migrations/versions/a821c186b426_add_host_tags.py, app/db/models.py, app/models/host.py
Adds host_tags and hosts_tags_association tables, HostTag SQLAlchemy model with tags/tag_ids on ProxyHost, and Pydantic HostTag/HostTagCreate/HostTagModify plus BaseHost.tags/tag_ids.
CRUD layer
app/db/crud/host.py
Adds resolve_host_tags, eager-loads tags in host queries, wires tag assignment into create_host/modify_host, adds host tag CRUD functions, and cascades tag-association deletes with host removal.
Operation validation and API routes
app/operation/host.py, app/routers/host.py
Adds validate_host_tags and tag CRUD operation methods (with 404/409 handling), and exposes GET/POST/PUT/DELETE /api/host/tags endpoints.
Dashboard shared constants and services
dashboard/src/constants/hostTagColors.ts, dashboard/src/service/api/index.ts, dashboard/src/service/hostTags.ts
Adds tag color style map, extends API types with tag fields, and adds REST functions plus React Query hooks for tags.
HostTagPicker and form integration
dashboard/src/features/hosts/components/host-tag-picker.tsx, dashboard/src/features/hosts/dialogs/host-modal.tsx, dashboard/src/features/hosts/forms/host-form.ts
Adds TagChip/HostTagPicker components for select/create/edit/delete, wires them into the host modal, and adds tag_ids to the host form schema/defaults.
Host list tag display
dashboard/src/features/hosts/components/hosts-list.tsx, .../sortable-host.tsx, .../use-hosts-list-columns.tsx
Renders tag chips and tint styling in grid/list/table views and propagates tag_ids through edit/duplicate/reorder payloads.
Backend tests
tests/api/test_host_tag.py, tests/test_host_tags_unit.py
Adds unit tests for tag schemas and end-to-end tests for tag CRUD, validation, host association, cascades, cloning, and bulk operations.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant HostRouter
  participant HostOperation
  participant HostCRUD
  participant Database

  Client->>HostRouter: POST /api/host/tags {name, color}
  HostRouter->>HostOperation: create_host_tag(new_tag, admin)
  HostOperation->>HostCRUD: get_host_tag_by_name(name)
  HostCRUD->>Database: SELECT host_tags WHERE name
  HostOperation->>HostCRUD: create_host_tag(new_tag)
  HostCRUD->>Database: INSERT host_tags
  HostOperation-->>HostRouter: HostTag
  HostRouter-->>Client: 201 Created

  Client->>HostRouter: POST /api/hosts {tag_ids}
  HostRouter->>HostOperation: create_host(new_host)
  HostOperation->>HostOperation: validate_host_tags(tag_ids)
  HostOperation->>HostCRUD: resolve_host_tags(tag_ids)
  HostCRUD->>Database: SELECT host_tags WHERE id IN tag_ids
  HostOperation->>HostCRUD: create_host(new_host)
  HostCRUD->>Database: INSERT hosts, hosts_tags_association
  HostOperation-->>HostRouter: Host (with tags)
  HostRouter-->>Client: 201 Created
Loading

Poem

A rabbit hops through fields of tags,
Red, blue, and slate in colorful bags. 🏷️
Hosts now wear their labels proud,
Chips and tints stand out from the crowd.
Hop, commit, and cascade with delight —
Tag it once, and it's just right! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: reusable, color-coded host tags for hosts.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Note

Unit test generation is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@ImMohammad20000

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Request timed out after 900000ms (requestId=942f6bc3-e3c9-4d82-aa0c-ab74bf19e866)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
dashboard/src/features/hosts/components/hosts-list.tsx (2)

184-184: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated tag_ids derivation into a shared helper.

The same host.tags?.map(tag => tag.id).filter((id): id is number => typeof id === 'number') ?? [] expression is duplicated in handleEdit, handleDuplicate, and the drag-reorder payload builder. Extracting a small helper (e.g. getHostTagIds(host: BaseHost): number[]) would reduce duplication and keep the three call sites in sync if this logic ever changes.

♻️ Proposed refactor
+const getHostTagIds = (host: BaseHost): number[] => host.tags?.map(tag => tag.id).filter((id): id is number => typeof id === 'number') ?? []
+
 const handleEdit = (host: BaseHost) => {
   ...
-      tag_ids: host.tags?.map(tag => tag.id).filter((id): id is number => typeof id === 'number') ?? [],
+      tag_ids: getHostTagIds(host),

(apply similarly at the handleDuplicate and drag-reorder call sites)

Also applies to: 372-372, 576-576

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/features/hosts/components/hosts-list.tsx` at line 184, The
`tag_ids` extraction logic is duplicated across `handleEdit`, `handleDuplicate`,
and the drag-reorder payload builder in `hosts-list.tsx`. Create a shared helper
like `getHostTagIds(host: BaseHost): number[]` that performs the current
`host.tags?.map(...).filter(...) ?? []` derivation, then replace each repeated
inline expression with that helper so all three call sites stay consistent.

984-987: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tint computation duplicated with sortable-host.tsx.

getTagColorStyle(host.tags[0].color).tint here mirrors the identical "use first tag's tint" logic in SortableHost (dashboard/src/features/hosts/components/sortable-host.tsx:48-49). Consider a small shared helper (e.g. getHostRowTint(host, selected)) so the grid/list/card views stay consistent if the tinting rule changes later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/features/hosts/components/hosts-list.tsx` around lines 984 -
987, The host row tint logic is duplicated between the hosts list and
SortableHost, so extract the “first tag tint unless selected” behavior into a
shared helper such as getHostRowTint(host, selected) and use it from the
rowClassName logic in hosts-list and the equivalent tint code in sortable-host.
Keep the helper responsible for checking selection and tags, then return the
tint from getTagColorStyle so both views stay consistent if the rule changes.
dashboard/src/service/hostTags.ts (1)

9-13: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Fragile substring-based cache invalidation.

Matching query keys by .includes('host') is brittle — any query key that happens to contain "host" as a substring gets invalidated (over-invalidation/perf cost), and if the actual host-list key is ever renamed to something without "host" in it, this silently stops working with no compile-time warning.

Prefer explicitly invalidating the known query keys (e.g., hostTagsQueryKey plus the host list key used in hosts-list.tsx, ['getGetHostsQueryKey']) instead of pattern matching.

♻️ Proposed fix
-const invalidateHostAndTagQueries = (queryClient: ReturnType<typeof useQueryClient>) =>
-  queryClient.invalidateQueries({
-    predicate: query =>
-      query.queryKey.some(part => typeof part === 'string' && part.toLowerCase().includes('host')),
-  })
+const invalidateHostAndTagQueries = (queryClient: ReturnType<typeof useQueryClient>) =>
+  Promise.all([
+    queryClient.invalidateQueries({ queryKey: hostTagsQueryKey }),
+    queryClient.invalidateQueries({ queryKey: ['getGetHostsQueryKey'], exact: true }),
+  ])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/service/hostTags.ts` around lines 9 - 13, The cache
invalidation in invalidateHostAndTagQueries relies on a brittle substring match
against query keys, which can over-invalidate unrelated queries and break
silently if keys change. Update this helper to explicitly invalidate the known
host-related keys used by the dashboard, including hostTagsQueryKey and the host
list query key from hosts-list.tsx (the getHosts query key), instead of using a
predicate with includes('host'). Ensure the queryClient.invalidateQueries call
targets those concrete keys directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dashboard/src/features/hosts/components/host-tag-picker.tsx`:
- Around line 125-130: The delete action in handleRemove should require explicit
user confirmation before calling removeTag.mutate, since the trash icon
permanently deletes a tag and can affect other hosts. Update host-tag-picker’s
delete flow around handleRemove and the trash button to open the app’s
AlertDialog confirmation pattern instead of deleting immediately, then only
invoke removeTag.mutate(id) after confirm; also disable the delete control while
removeTag.isPending to prevent duplicate requests.

---

Nitpick comments:
In `@dashboard/src/features/hosts/components/hosts-list.tsx`:
- Line 184: The `tag_ids` extraction logic is duplicated across `handleEdit`,
`handleDuplicate`, and the drag-reorder payload builder in `hosts-list.tsx`.
Create a shared helper like `getHostTagIds(host: BaseHost): number[]` that
performs the current `host.tags?.map(...).filter(...) ?? []` derivation, then
replace each repeated inline expression with that helper so all three call sites
stay consistent.
- Around line 984-987: The host row tint logic is duplicated between the hosts
list and SortableHost, so extract the “first tag tint unless selected” behavior
into a shared helper such as getHostRowTint(host, selected) and use it from the
rowClassName logic in hosts-list and the equivalent tint code in sortable-host.
Keep the helper responsible for checking selection and tags, then return the
tint from getTagColorStyle so both views stay consistent if the rule changes.

In `@dashboard/src/service/hostTags.ts`:
- Around line 9-13: The cache invalidation in invalidateHostAndTagQueries relies
on a brittle substring match against query keys, which can over-invalidate
unrelated queries and break silently if keys change. Update this helper to
explicitly invalidate the known host-related keys used by the dashboard,
including hostTagsQueryKey and the host list query key from hosts-list.tsx (the
getHosts query key), instead of using a predicate with includes('host'). Ensure
the queryClient.invalidateQueries call targets those concrete keys directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2cd94b48-ba0e-4956-bd23-c58b4ef7a899

📥 Commits

Reviewing files that changed from the base of the PR and between a8ae6b6 and 92decfb.

📒 Files selected for processing (17)
  • app/db/crud/host.py
  • app/db/migrations/versions/a821c186b426_add_host_tags.py
  • app/db/models.py
  • app/models/host.py
  • app/operation/host.py
  • app/routers/host.py
  • dashboard/src/constants/hostTagColors.ts
  • dashboard/src/features/hosts/components/host-tag-picker.tsx
  • dashboard/src/features/hosts/components/hosts-list.tsx
  • dashboard/src/features/hosts/components/sortable-host.tsx
  • dashboard/src/features/hosts/components/use-hosts-list-columns.tsx
  • dashboard/src/features/hosts/dialogs/host-modal.tsx
  • dashboard/src/features/hosts/forms/host-form.ts
  • dashboard/src/service/api/index.ts
  • dashboard/src/service/hostTags.ts
  • tests/api/test_host_tag.py
  • tests/test_host_tags_unit.py

Comment on lines +125 to +130
const handleRemove = (id: number) => {
removeTag.mutate(id, {
onSuccess: () => onChange(value.filter(x => x !== id)),
onError: () => toast.error(t('hostTags.deleteFailed', { defaultValue: 'Failed to delete tag' })),
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add a confirmation step before deleting a tag.

handleRemove immediately calls removeTag.mutate(id) with no confirmation. Unlike the chip's "X" (which only toggles selection locally via toggle), this trash icon permanently deletes the tag record, which per the PR description cascades and un-links it from every host that currently uses it. A stray click inside a host's edit form can silently affect unrelated hosts with no way to undo. Also consider disabling the delete button while removeTag.isPending to avoid duplicate delete requests from rapid clicks.

🛡️ Proposed fix: confirm before delete
-  const handleRemove = (id: number) => {
-    removeTag.mutate(id, {
-      onSuccess: () => onChange(value.filter(x => x !== id)),
-      onError: () => toast.error(t('hostTags.deleteFailed', { defaultValue: 'Failed to delete tag' })),
-    })
-  }
+  const handleRemove = (id: number, name: string) => {
+    if (removeTag.isPending) return
+    if (!window.confirm(t('hostTags.confirmDelete', { name, defaultValue: `Delete tag "${name}"? It will be removed from all hosts.` }))) {
+      return
+    }
+    removeTag.mutate(id, {
+      onSuccess: () => onChange(value.filter(x => x !== id)),
+      onError: () => toast.error(t('hostTags.deleteFailed', { defaultValue: 'Failed to delete tag' })),
+    })
+  }

A dedicated AlertDialog (consistent with the app's existing bulk-action confirmation pattern) would be preferable to window.confirm for styling consistency.

Also applies to: 194-199

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/features/hosts/components/host-tag-picker.tsx` around lines 125
- 130, The delete action in handleRemove should require explicit user
confirmation before calling removeTag.mutate, since the trash icon permanently
deletes a tag and can affect other hosts. Update host-tag-picker’s delete flow
around handleRemove and the trash button to open the app’s AlertDialog
confirmation pattern instead of deleting immediately, then only invoke
removeTag.mutate(id) after confirm; also disable the delete control while
removeTag.isPending to prevent duplicate requests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants