Dashboard Comments
Overview
The Dashboard Comments feature allows users to create and manage comments directly within dashboards in the HelloDATA Portal. Comments support a full publishing workflow with draft and published states, versioning, and role-based access control.
Architecture
Components
Backend (Spring Boot)
- DashboardCommentController - REST API controller (
/dashboards/{contextKey}/{dashboardId}/comments) - DashboardCommentService - Business logic and permission validation
- DashboardCommentPermissionService - Permission management and synchronization
- DashboardCommentRepository - JPA repository for comment database operations
- DashboardCommentPermissionRepository - JPA repository for permission database operations
- DashboardCommentEntity - JPA entity for comment metadata
- DashboardCommentVersionEntity - JPA entity for versioned content
- DashboardCommentPermissionEntity - JPA entity for user permissions per data domain
Frontend (Angular)
- CommentsFeed - Main comments panel component
- CommentEntryComponent - Individual comment display with actions
- DomainDashboardCommentsComponent - Aggregated view of all comments for a domain
- DashboardCommentUtilsService - Shared utilities for comment operations
- NgRx Store - State management for comments
Data Model
The data model consists of three main entities:
- DashboardCommentEntry - Immutable comment metadata
- DashboardCommentVersion - Versioned content with history
- DashboardCommentPermission - User permissions per data domain
Key Design Points:
pointerUrlandtagsare stored per-version and derived from the active version when displaying- Permissions are independent of comment data and stored in a separate table
- Hierarchical permission model (REVIEW ⊃ WRITE ⊃ READ) is enforced at application layer
┌─────────────────────────────────────────────────────────────┐
│ DashboardCommentEntry │
├─────────────────────────────────────────────────────────────┤
│ id: string (UUID) │
│ dashboardId: number │
│ dashboardUrl: string │
│ contextKey: string │
│ author: string │
│ authorEmail: string │
│ createdDate: number (timestamp) │
│ deleted: boolean │
│ deletedDate?: number │
│ deletedBy?: string │
│ activeVersion: number │
│ hasActiveDraft?: boolean │
│ entityVersion: number (for optimistic locking) │
│ importedFromId?: string (original ID when imported) │
│ history: DashboardCommentVersion[] │
│ pointerUrl?: string (derived from active version) │
│ tags?: string[] (derived from active version) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ DashboardCommentVersion │
├─────────────────────────────────────────────────────────────┤
│ version: number │
│ text: string │
│ status: DashboardCommentStatus │
│ (DRAFT | READY_FOR_REVIEW | PUBLISHED | DECLINED │
│ | DELETED) │
│ editedDate: number │
│ editedBy: string │
│ publishedDate?: number │
│ publishedBy?: string │
│ declineReason?: string │
│ deleted: boolean │
│ pointerUrl?: string (specific page/chart link for version) │
│ tags?: string[] (tags snapshot for this version) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ DashboardCommentPermission │
├─────────────────────────────────────────────────────────────┤
│ id: string (UUID) │
│ userId: string (UUID) │
│ contextKey: string │
│ readComments: boolean │
│ writeComments: boolean │
│ reviewComments: boolean │
│ createdDate?: number (timestamp) │
│ createdBy?: string │
│ modifiedDate?: number (timestamp) │
│ modifiedBy?: string │
└─────────────────────────────────────────────────────────────┘
Relationships:
DashboardCommentEntry→DashboardCommentVersion: One-to-Many (viahistoryarray)DashboardCommentPermission→User: Many-to-One (viauserId)DashboardCommentPermission→Context: Many-to-One (viacontextKey)- Unique constraint: One permission record per
(userId, contextKey)pair
Note: The top-level
pointerUrlandtagsfields onDashboardCommentEntryare derived from the active version in history. They are not stored separately on the comment entity. This ensures that when admins switch versions (e.g.,restoreVersion), the displayedpointerUrlandtagsalways match the active version's data.Permission Independence: The
DashboardCommentPermissionentity is completely independent from comment data, allowing administrators to manage commenting permissions separately from comment content. Permissions are automatically created during user synchronization and can be manually adjusted per user per data domain.
User Roles and Permissions
Permission System
The dashboard commenting system uses a three-level permission model that is independent of data domain roles:
| Permission Level | Description | What it includes |
|---|---|---|
| READ | View published comments only | Basic read-only access |
| WRITE | Create and manage own comments | READ + create/edit own comments + view own drafts + delete own comments |
| REVIEW | Full comment moderation capabilities | WRITE + view/manage all drafts + publish/decline + delete |
Key Points:
- Permissions are stored per user per data domain in
dashboard_comment_permissiontable - Independent of data domain roles - a user's commenting permissions can differ from their data access level
- Hierarchical - REVIEW includes WRITE capabilities, WRITE includes READ capabilities
- Flexible - administrators can assign custom permission combinations for fine-grained control
Permission Matrix
The table below shows what each permission level allows:
| Action | READ | WRITE | REVIEW |
|---|---|---|---|
| View published comments | ✔ | ✔ | ✔ |
| View own drafts/declined | ✔ | ✔ | |
| View all drafts/declined | ✔ | ||
| Create comment | ✔ | ✔ | |
| Edit own comment | ✔ | ✔ | |
| Edit any comment | ✔ | ||
| Delete own version/entire | ✔ | ✔ | |
| Delete any version/entire | ✔ | ||
| Send for review | ✔ | ✔ | |
| Publish comment | ✔ | ||
| Decline comment | ✔ | ||
| View metadata & versions | ✔ | ||
| Restore version* | ✔ |
Legend:
- ✔ = Action allowed
- (empty) = Action not allowed
- * = Cannot restore version when comment is in READY_FOR_REVIEW status
Note for Administrators: HELLODATA_ADMIN, BUSINESS_DOMAIN_ADMIN, and DATA_DOMAIN_ADMIN roles act as templates for default permissions. Their permissions are synced to the
dashboard_comment_permissiontable, which is the primary source of truth for all authorization checks.Note: Dashboard-level access (Superset RBAC) is also validated for restricted roles like
DATA_DOMAIN_VIEWER. Even if they have READ permission for comments in the domain, they can only see comments for dashboards they can access in Superset.
Automatic Permission Assignment
When users are synchronized or new domains are created, default permissions are automatically assigned based on portal roles:
| Portal Role | Default Comment Permissions | Scope |
|---|---|---|
| HELLODATA_ADMIN | READ + WRITE + REVIEW | All data domains |
| BUSINESS_DOMAIN_ADMIN | READ + WRITE + REVIEW | All data domains |
| DATA_DOMAIN_ADMIN | READ + WRITE + REVIEW | Specific data domain |
| Regular Users | READ only | All accessible domains |
| NONE role | No access | Specific data domain |
Important: These are default assignments. Administrators can manually adjust individual user permissions through the User Management UI or database to grant custom access levels.
Permission Validation
Backend (DashboardCommentService)
The backend validates permissions solely by checking the dashboard_comment_permission table:
// Check user's comment permissions for a context
private void checkHasAccessToComment(String contextKey) {
DashboardCommentPermissionEntity perm = getCommentPermissionForCurrentUser(contextKey);
if (perm == null || !perm.isWriteComments()) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "No write permission for comments");
}
}
Frontend (NgRx Selectors)
Frontend selectors check permissions from the NgRx store state:
// Check if user can publish comments (requires REVIEW permission)
export const canPublishComment = createSelector(
selectCurrentDashboardContextKey,
selectCurrentUserCommentPermissions,
selectIsSuperuser,
selectIsBusinessDomainAdmin,
(state: AppState) => state.auth,
(contextKey, permissions, isSuperuser, isBusinessDomainAdmin, authState) => {
// Admins always have full access
if (isSuperuser || isBusinessDomainAdmin) {
return true;
}
// Check if user is data domain admin for this context
const isDataDomainAdmin = authState.contextRoles.some(role =>
role.context.contextKey === contextKey &&
role.role.name === DATA_DOMAIN_ADMIN_ROLE
);
if (isDataDomainAdmin) {
return true;
}
// Check REVIEW permission from permission table
return permissions?.reviewComments || false;
}
);
// Check if user can create/edit comments (requires WRITE permission)
export const canEditComment = createSelector(
selectCurrentDashboardContextKey,
selectCurrentUserCommentPermissions,
selectCurrentUserEmail,
(contextKey, permissions, currentUserEmail) => (comment) => {
// User can edit their own comments with WRITE permission
if (comment.authorEmail === currentUserEmail && permissions?.writeComments) {
return true;
}
// Or edit any comment with REVIEW permission
return permissions?.reviewComments || false;
}
);
Comment Lifecycle
States
┌──────────┐ send for review ┌──────────────────┐ publish ┌───────────┐
│ DRAFT │ ──────────────────► │ READY_FOR_REVIEW │ ─────────────► │ PUBLISHED │
└──────────┘ └──────────────────┘ └───────────┘
▲ │ │
│ decline │ │
└───────────────────────────────────┘ │
│
▲ │
│ edit published │
└─────────────────────────────────────────────────────────────────────┘
Workflow
- Create Comment - User creates a comment (status: DRAFT, version: 1)
- Edit Draft - Author or admin can edit the draft text
- Send for Review - Author sends the draft for moderation (status: READY_FOR_REVIEW)
- Decline - Reviewer declines the draft with a reason (status: DECLINED)
- Publish - Reviewer publishes the comment (status: PUBLISHED)
- Edit Published - Creates a new DRAFT version while keeping the published version active
- Delete - Soft deletes current version; restores last published if available. If
deleteEntireis true, soft deletes the entire comment entity.
Versioning
Each edit to a published comment creates a new version:
Version 1 (PUBLISHED) ─► Version 2 (DRAFT) ─► Version 2 (PUBLISHED) ─► Version 3 (DRAFT)
│
(activeVersion = 3)
- activeVersion - Points to the currently active version
- hasActiveDraft - True when a draft version exists
- Admins can restore any non-deleted PUBLISHED version
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET |
/dashboards/{contextKey}/{dashboardId}/comments |
Get all visible comments |
POST |
/dashboards/{contextKey}/{dashboardId}/comments |
Create new comment |
PUT |
/dashboards/{contextKey}/{dashboardId}/comments/{commentId} |
Update draft comment |
DELETE |
/dashboards/{contextKey}/{dashboardId}/comments/{commentId} |
Delete comment version or entire |
POST |
/dashboards/{contextKey}/{dashboardId}/comments/{commentId}/send-for-review |
Send draft for review |
POST |
/dashboards/{contextKey}/{dashboardId}/comments/{commentId}/decline |
Decline draft with reason |
POST |
/dashboards/{contextKey}/{dashboardId}/comments/{commentId}/publish |
Publish comment |
POST |
/dashboards/{contextKey}/{dashboardId}/comments/{commentId}/clone |
Clone for edit (creates new version) |
POST |
/dashboards/{contextKey}/{dashboardId}/comments/{commentId}/restore/{versionNumber} |
Restore specific version |
GET |
/dashboards/{contextKey}/{dashboardId}/comments/tags |
Get all tags for dashboard |
GET |
/dashboards/{contextKey}/{dashboardId}/comments/export |
Export comments to JSON |
POST |
/dashboards/{contextKey}/{dashboardId}/comments/import |
Import comments from JSON |
Request/Response Examples
Create Comment
// POST /dashboards/demo/5/comments
// Request:
{
"text": "This dashboard shows sales metrics",
"dashboardUrl": "https://superset.example.com/superset/dashboard/5/",
"pointerUrl": "https://superset.example.com/superset/dashboard/5/?tab=sales"
}
// Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"dashboardId": 5,
"contextKey": "demo",
"author": "John Doe",
"authorEmail": "john.doe@example.com",
"createdDate": 1705827600000,
"activeVersion": 1,
"entityVersion": 0,
"history": [
{
"version": 1,
"text": "This dashboard shows sales metrics",
"status": "DRAFT",
"editedDate": 1705827600000,
"editedBy": "John Doe",
"deleted": false
}
]
}
Update Comment
// PUT /dashboards/demo/5/comments/550e8400-e29b-41d4-a716-446655440000
// Request:
{
"text": "Updated comment text",
"entityVersion": 0
}
Optimistic Locking
To prevent concurrent edit conflicts, the system uses optimistic locking via entityVersion:
- Client sends
entityVersionwith update request - Backend verifies
entityVersionmatches current value - If mismatch, returns
409 Conflicterror - Client receives notification and refreshes comments
private void checkEntityVersion(DashboardCommentEntity comment, Integer providedVersion, String userEmail) {
if (providedVersion != null && !providedVersion.equals(comment.getEntityVersion())) {
log.warn("Optimistic lock conflict for comment {}, current: {}, provided: {}",
comment.getId(), comment.getEntityVersion(), providedVersion);
throw new ResponseStatusException(HttpStatus.CONFLICT,
"Comment was modified by another user. Please refresh and try again.");
}
}
Visibility Rules
For Regular Users (READ)
- See all PUBLISHED comments
- If current activeVersion is a DRAFT, READY_FOR_REVIEW or DECLINED by someone else → show last PUBLISHED version instead
- If no PUBLISHED version exists → comment is hidden
For Authors (WRITE)
- See all rules for Regular Users
- See own DRAFT, READY_FOR_REVIEW, and DECLINED versions
- Author can see their own declined version to read the decline reason
For Reviewers (REVIEW)
- See all comments (DRAFT, READY_FOR_REVIEW, PUBLISHED, DECLINED)
- See complete version history
- Can switch between versions for any comment
Dashboard Access Restriction
Users with certain roles (e.g., DATA_DOMAIN_VIEWER, BUSINESS_SPECIALIST) are restricted to seeing comments only on
dashboards they have explicit access to via Superset RBAC. This is enforced by the backend by matching user roles
against the required roles for each dashboard.
Consolidated Deletion Logic
The deletion process has been simplified into a single "Delete" action:
- Delete Version (Default): By default, the delete action soft-deletes the current active version (sets status to
DELETED).- If a previous
PUBLISHEDversion exists, the comment is restored to that version. - If no published version exists, the entire comment is soft-deleted.
- If a previous
- Delete Entire Comment: If the user explicitly selects "Delete entire comment", the entire comment entity is soft-deleted regardless of version history.
Authors can delete their own DRAFT or DECLINED versions. Reviewers can delete any version or entire comments.
Features
Pointer URL
Comments can include an optional pointerUrl that links to a specific dashboard page, tab, or chart:
- Validated to ensure it points to the same Superset instance
- Clicking the link loads that specific view in the iframe
Tags
Comments can be tagged with short labels for better organization and filtering:
- Scope: Tags are scoped per dashboard - each dashboard has its own set of tags
- Length: Maximum 10 characters per tag
- Normalization: Tags are automatically trimmed, lowercased, and deduplicated
- Multiple tags: A comment can have multiple tags assigned
- Autocomplete: When adding tags, existing tags from the dashboard are suggested
- Filtering: Comments can be filtered by tag in the comments panel
- Permissions: Adding/editing tags follows the same permission rules as editing comments
- History tracking: Each version stores a snapshot of tags - changes to tags are visible in version history
Tag Display
- Tags are displayed at the bottom of each comment entry
- Each tag shows with a tag icon (
fa-solid fa-tag) followed by the tag name - Tags have a distinctive blue chip styling for easy identification
- In version history, tags for each version are shown, allowing comparison of tag changes
Tag API
Returns all unique tags used in comments for a specific dashboard.
- Useful for referencing specific parts of complex dashboards
Filtering
Domain comments view supports filtering by:
- Year
- Quarter (based on comment creation date)
- Tag (if any comments have tags assigned)
- Text search (searches comment text content)
Auto-refresh
Comments are automatically refreshed every 30 seconds when viewing a dashboard.
Database Schema
Main Tables
CREATE TABLE dashboard_comment
(
id VARCHAR(36) PRIMARY KEY,
dashboard_id INTEGER NOT NULL,
dashboard_url VARCHAR(2048),
context_key VARCHAR(255) NOT NULL,
author VARCHAR(255),
author_email VARCHAR(255),
created_date BIGINT,
deleted BOOLEAN DEFAULT FALSE,
deleted_date BIGINT,
deleted_by VARCHAR(255),
active_version INTEGER,
has_active_draft BOOLEAN DEFAULT FALSE,
entity_version INTEGER DEFAULT 0,
imported_from_id VARCHAR(36) -- Original ID when imported from another dashboard
);
CREATE TABLE dashboard_comment_version
(
id BIGSERIAL PRIMARY KEY,
comment_id VARCHAR(36) REFERENCES dashboard_comment (id),
version INTEGER NOT NULL,
text TEXT,
status VARCHAR(20),
edited_date BIGINT,
edited_by VARCHAR(255),
published_date BIGINT,
published_by VARCHAR(255),
deleted BOOLEAN DEFAULT FALSE,
tags TEXT, -- Comma-separated tags snapshot for this version
pointer_url VARCHAR(2000) -- Optional link to specific page/chart for this version
);
CREATE TABLE dashboard_comment_permission
(
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
context_key VARCHAR(100) NOT NULL,
read_comments BOOLEAN NOT NULL DEFAULT false,
write_comments BOOLEAN NOT NULL DEFAULT false,
review_comments BOOLEAN NOT NULL DEFAULT false,
created_date TIMESTAMP,
created_by VARCHAR(255),
modified_date TIMESTAMP,
modified_by VARCHAR(255),
CONSTRAINT uq_dashboard_comment_permission_user_context UNIQUE (user_id, context_key),
CONSTRAINT fk_dashboard_comment_permission_user FOREIGN KEY (user_id)
REFERENCES user_ (id) ON DELETE CASCADE,
CONSTRAINT fk_dashboard_comment_permission_context FOREIGN KEY (context_key)
REFERENCES context (context_key) ON DELETE CASCADE
);
-- Indexes for comment lookups
CREATE INDEX idx_dashboard_comment_context_dashboard
ON dashboard_comment (context_key, dashboard_id);
-- Index for import duplicate detection
CREATE INDEX idx_comment_imported_from
ON dashboard_comment (imported_from_id, context_key, dashboard_id);
-- Indexes for permission lookups
CREATE INDEX idx_comment_permission_user
ON dashboard_comment_permission (user_id);
CREATE INDEX idx_comment_permission_context
ON dashboard_comment_permission (context_key);
Table Descriptions
dashboard_comment
Main comment entity storing metadata and reference to active version. Uses optimistic locking via entity_version.
dashboard_comment_version
Stores complete version history including text, status, tags, and pointer URLs. Each version is immutable once created.
dashboard_comment_permission
Stores per-user per-data-domain commenting permissions. Independent of comment data, allowing flexible permission management.
Note:
pointer_urlandtagsare stored per version indashboard_comment_version, not on the maindashboard_commenttable. This design ensures each version snapshot captures the complete state (text, tags, pointerUrl) at that point in time. The application derives the currentpointerUrlandtagsfrom the active version when building DTOs.
Permission Table Details
The dashboard_comment_permission table provides fine-grained control over commenting capabilities:
- Unique constraint on
(user_id, context_key)ensures one permission record per user per domain - Cascade delete on user and context ensures automatic cleanup
- Boolean flags represent the three permission levels (read, write, review)
- Hierarchical permissions are enforced by the application layer (review → write → read)
- Audit fields track when and by whom permissions were created/modified
- Independent storage allows permission management without affecting comment data
Permission Management and Synchronization
Automatic Permission Initialization
Initial Setup via Liquibase Migration
When the system is first deployed or upgraded, a Liquibase migration (55_initialize_dashboard_comment_permissions.sql)
automatically creates default permissions for all existing users:
Migration Logic:
- Admins (HELLODATA_ADMIN, BUSINESS_DOMAIN_ADMIN) → Full access (READ + WRITE + REVIEW) in all domains
- Data Domain Admins (DATA_DOMAIN_ADMIN) → Full access (READ + WRITE + REVIEW) in their specific domain
- Regular users → Read-only access (READ) in all domains
The migration uses SQL JOINs with user_portal_role and user_context_role tables to determine user roles and is *
idempotent* - running it multiple times will not create duplicates.
User Synchronization (syncAllUsers)
The syncAllUsers() method in UserService automatically maintains comment permissions when users are synchronized:
// In UserService.syncAllUsers()
dashboardCommentPermissionService.syncDefaultPermissionsForUser(userEntity);
When it runs:
- During scheduled user synchronization
- When new users are created
- When new data domains are added
What it does:
- Checks if user has admin roles (HELLODATA_ADMIN or BUSINESS_DOMAIN_ADMIN)
- Creates missing permissions for any data domains where user doesn't have permissions yet
- Never modifies existing permissions - only creates new ones for new domains
- Logs the number of permissions created for each user
Permission Assignment:
if(isAdmin){
// Admins get full access
permission.
setReadComments(true);
permission.
setWriteComments(true);
permission.
setReviewComments(true);
}else{
// Regular users get read-only
permission.
setReadComments(true);
permission.
setWriteComments(false);
permission.
setReviewComments(false);
}
Use Cases for Automatic Synchronization
1. New Data Domain Added
Scenario: Administrator creates a new data domain "finance"
Result: syncAllUsers() runs automatically
- All admins receive READ+WRITE+REVIEW for "finance"
- All regular users receive READ for "finance"
2. New User Created
Scenario: New user "john@example.com" is added to the system
Result: syncAllUsers() processes the new user
- Permissions created for all existing data domains
- Permission level based on user's portal role
3. User Promoted to Admin
Scenario: Regular user is promoted to BUSINESS_DOMAIN_ADMIN
Result: syncAllUsers() does NOT automatically upgrade existing permissions
Action: Administrator must manually update permissions via database
OR re-run migration to reset all permissions
Best Practices
- New Domains: Always run user sync after creating new data domains to ensure all users get appropriate permissions
- Role Changes: When promoting users to admin roles, consider manually updating their comment permissions if immediate access is needed
- Permission Auditing: Use
created_byandmodified_byfields to track permission changes - Regular Sync: Schedule regular
syncAllUsers()execution to catch any missing permissions
Troubleshooting
Problem: User cannot see comment actions despite having correct domain role
Solution:
- Check
dashboard_comment_permissiontable for user's permissions - Verify
context_keymatches the current dashboard's domain - Run
syncAllUsers()to create missing permissions - Check application logs for permission validation errors
SQL Query to Check Permissions:
SELECT u.email,
dcp.context_key,
dcp.read_comments,
dcp.write_comments,
dcp.review_comments
FROM dashboard_comment_permission dcp
JOIN user_ u ON dcp.user_id = u.id
WHERE u.email = 'user@example.com';
Error Handling
| HTTP Status | Scenario |
|---|---|
403 Forbidden |
User lacks permission for the action |
404 Not Found |
Comment not found |
409 Conflict |
Optimistic locking conflict (concurrent edit) |
Import/Export
Comments can be exported and imported between dashboards for migration, backup, or copying comments across environments.
Export
- Format: JSON file
- Scope: Exports all non-deleted comments with complete version history (both DRAFT and PUBLISHED)
- Contents: Comment ID, text, author, creation date, status, tags, activeVersion, and full version history
- Permission: Only admins (superuser, business_domain_admin, data_domain_admin) can export
- File naming:
comments_{contextKey}_{dashboardTitle}_{date}.json(title is sanitized for filesystem)
Export Format
{
"exportVersion": "1.0",
"contextKey": "demo",
"dashboardId": 5,
"dashboardTitle": "Sales Dashboard",
"exportDate": 1705827600000,
"comments": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"text": "Current active version text",
"author": "John Doe",
"authorEmail": "john.doe@example.com",
"createdDate": 1705827600000,
"status": "PUBLISHED",
"activeVersion": 2,
"tags": [
"sales",
"kpi"
],
"history": [
{
"version": 1,
"text": "First version text",
"status": "PUBLISHED",
"editedDate": 1705827600000,
"editedBy": "John Doe",
"publishedDate": 1705828000000,
"publishedBy": "Admin User",
"tags": [
"sales"
]
},
{
"version": 2,
"text": "Current active version text",
"status": "PUBLISHED",
"editedDate": 1705830000000,
"editedBy": "Jane Smith",
"publishedDate": 1705831000000,
"publishedBy": "Admin User",
"tags": [
"sales",
"kpi"
]
}
]
}
]
}
Import
- Permission: Only admins (superuser, business_domain_admin, data_domain_admin) can import
- Status preservation: Comments are imported with their original status (DRAFT or PUBLISHED) preserved
- History preservation: Full version history is imported, allowing admins to review previous versions
- Pointer URL: Preserved per-version from export file (stored on each version in history)
- Dashboard URL: Automatically set to the target dashboard URL (not copied from source)
- Tags: Preserved per-version from export file
- Author: Preserved from export if available, otherwise uses importing user
Import Scenarios
1. First Import (Same Dashboard - Re-import)
When importing to the same dashboard where comments were originally exported:
- Comments with matching IDs are updated with imported data
- New comments (no matching ID) are created
- Prevents duplicate comments on re-import
2. Import from Different Dashboard
When importing to a different dashboard:
- New comments are created with new IDs
- The
importedFromIdfield tracks the original comment ID - On subsequent imports of the same file, comments are updated (not duplicated) based on
importedFromId
3. Cross-Environment Import
When importing from another environment (e.g., DEV → PROD):
- Comments are created with new IDs
- Original author information is preserved
importedFromIdenables re-import without duplicates
Import Result
{
"imported": 5,
"updated": 2,
"skipped": 1,
"message": "Imported 5 new, updated 2 existing, skipped 1"
}
- imported: Number of new comments created
- updated: Number of existing comments updated
- skipped: Number of invalid items (e.g., empty text)
Import Validation
- File must be valid JSON
- Comments without text are skipped
- Invalid tags are normalized or skipped
- Dashboard URL is replaced with target dashboard URL
Import/Export Use Cases
- Backup: Export comments before major changes
- Migration: Move comments when restructuring dashboards
- Template: Create standard comments that can be imported to multiple dashboards
- Cross-environment: Copy comments from development to production environment
- Disaster recovery: Restore comments from backup after data loss
Import Tracking (importedFromId)
The importedFromId field in the database tracks the original comment ID from import. This enables:
- Detection of previously imported comments to prevent duplicates
- Traceability of comment origins
- Safe re-imports that update existing comments instead of creating duplicates
Mobile Support
The comments panel adapts to mobile view:
- Comments panel opens as a drawer from the top
- Simplified UI for touch interaction
- Full functionality preserved