diff --git a/internal/collector/todos.go b/internal/collector/todos.go index c65157b..977ca7b 100644 --- a/internal/collector/todos.go +++ b/internal/collector/todos.go @@ -31,14 +31,16 @@ func clipTo(s string, n int) string { } type todoPayload struct { - Project string `json:"project"` - Text string `json:"text"` - Component string `json:"component"` - Path string `json:"path"` - Hash string `json:"hash"` - URL string `json:"url"` - UserAgent string `json:"ua"` - Session string `json:"session"` + Project string `json:"project"` + Text string `json:"text"` + Component string `json:"component"` + Path string `json:"path"` + Hash string `json:"hash"` + URL string `json:"url"` + UserAgent string `json:"ua"` + Session string `json:"session"` + ElementSelector string `json:"elementSelector"` + ElementLabel string `json:"elementLabel"` } // HandleTodoCreate ingests a todo posted by otel-todo.js. It is guarded by @@ -72,16 +74,18 @@ func (c *Collector) HandleTodoCreate(w http.ResponseWriter, r *http.Request) { id := make([]byte, 8) _, _ = rand.Read(id) t := todos.Todo{ - ID: hex.EncodeToString(id), - Project: project, - Text: text, - Component: clipTo(p.Component, maxTodoMeta), - Path: clipTo(p.Path, maxTodoMeta), - Hash: clipTo(p.Hash, maxTodoMeta), - URL: clipTo(p.URL, maxTodoMeta), - UserAgent: clipTo(p.UserAgent, maxTodoMeta), - Session: clipTo(p.Session, maxTodoMeta), - CreatedAt: time.Now().UTC(), + ID: hex.EncodeToString(id), + Project: project, + Text: text, + Component: clipTo(p.Component, maxTodoMeta), + Path: clipTo(p.Path, maxTodoMeta), + Hash: clipTo(p.Hash, maxTodoMeta), + URL: clipTo(p.URL, maxTodoMeta), + UserAgent: clipTo(p.UserAgent, maxTodoMeta), + Session: clipTo(p.Session, maxTodoMeta), + ElementSelector: clipTo(p.ElementSelector, maxTodoMeta), + ElementLabel: clipTo(p.ElementLabel, maxTodoMeta), + CreatedAt: time.Now().UTC(), } if err := c.todoStore.Add(r.Context(), t); err != nil { log.Printf("failed to store todo: %v", err) diff --git a/internal/todos/todos.go b/internal/todos/todos.go index 0c2ad70..77184eb 100644 --- a/internal/todos/todos.go +++ b/internal/todos/todos.go @@ -17,18 +17,20 @@ import ( // Todo is one note filed from a page. Project is the RUM service name. type Todo struct { - ID string `json:"id"` - Project string `json:"project"` - Text string `json:"text"` - Component string `json:"component,omitempty"` - Path string `json:"path,omitempty"` - Hash string `json:"hash,omitempty"` - URL string `json:"url,omitempty"` - UserAgent string `json:"userAgent,omitempty"` - Session string `json:"session,omitempty"` - CreatedAt time.Time `json:"createdAt"` - Done bool `json:"done"` - DoneAt time.Time `json:"doneAt,omitzero"` + ID string `json:"id"` + Project string `json:"project"` + Text string `json:"text"` + Component string `json:"component,omitempty"` + Path string `json:"path,omitempty"` + Hash string `json:"hash,omitempty"` + URL string `json:"url,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + Session string `json:"session,omitempty"` + ElementSelector string `json:"elementSelector,omitempty"` // CSS selector of the referenced element, if picked + ElementLabel string `json:"elementLabel,omitempty"` // human-readable tag/text summary of that element + CreatedAt time.Time `json:"createdAt"` + Done bool `json:"done"` + DoneAt time.Time `json:"doneAt,omitzero"` } // Store persists todos. List returns newest first; an empty project means all. @@ -154,7 +156,7 @@ func NewClickHouseStore(ctx context.Context, conn driver.Conn, db string) (*Clic if err != nil { return nil, err } - for _, col := range []string{"done UInt8", "done_at DateTime64(3, 'UTC')"} { + for _, col := range []string{"done UInt8", "done_at DateTime64(3, 'UTC')", "element_selector String", "element_label String"} { if err := conn.Exec(ctx, fmt.Sprintf(`ALTER TABLE %s.todos ADD COLUMN IF NOT EXISTS %s`, db, col)); err != nil { return nil, err } @@ -163,12 +165,12 @@ func NewClickHouseStore(ctx context.Context, conn driver.Conn, db string) (*Clic } func (s *ClickHouseStore) Add(ctx context.Context, t Todo) error { - return s.conn.Exec(ctx, fmt.Sprintf(`INSERT INTO %s.todos (id, project, text, component, path, hash, url, user_agent, session, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, s.db), - t.ID, t.Project, t.Text, t.Component, t.Path, t.Hash, t.URL, t.UserAgent, t.Session, t.CreatedAt.UTC()) + return s.conn.Exec(ctx, fmt.Sprintf(`INSERT INTO %s.todos (id, project, text, component, path, hash, url, user_agent, session, element_selector, element_label, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, s.db), + t.ID, t.Project, t.Text, t.Component, t.Path, t.Hash, t.URL, t.UserAgent, t.Session, t.ElementSelector, t.ElementLabel, t.CreatedAt.UTC()) } func (s *ClickHouseStore) List(ctx context.Context, project string, limit int) ([]Todo, error) { - q := fmt.Sprintf(`SELECT id, project, text, component, path, hash, url, user_agent, session, created_at, done, done_at FROM %s.todos`, s.db) + q := fmt.Sprintf(`SELECT id, project, text, component, path, hash, url, user_agent, session, element_selector, element_label, created_at, done, done_at FROM %s.todos`, s.db) var args []any if project != "" { q += ` WHERE project = ?` @@ -185,7 +187,7 @@ func (s *ClickHouseStore) List(ctx context.Context, project string, limit int) ( for rows.Next() { var t Todo var done uint8 - if err := rows.Scan(&t.ID, &t.Project, &t.Text, &t.Component, &t.Path, &t.Hash, &t.URL, &t.UserAgent, &t.Session, &t.CreatedAt, &done, &t.DoneAt); err != nil { + if err := rows.Scan(&t.ID, &t.Project, &t.Text, &t.Component, &t.Path, &t.Hash, &t.URL, &t.UserAgent, &t.Session, &t.ElementSelector, &t.ElementLabel, &t.CreatedAt, &done, &t.DoneAt); err != nil { return nil, err } t.Done = done != 0 diff --git a/otel-ui/src/components/Tabs.tsx b/otel-ui/src/components/Tabs.tsx index dd2d3b0..21e7061 100644 --- a/otel-ui/src/components/Tabs.tsx +++ b/otel-ui/src/components/Tabs.tsx @@ -7,6 +7,7 @@ import { CheckCircle2, ChevronDown, ChevronUp, + X, } from "lucide-react" import type { LucideIcon } from "lucide-react" import { MetricsVisualizer } from "./MetricsVisualizer" @@ -398,11 +399,27 @@ export const TabsComponent = () => { {statusNotice && (
- {statusNotice.text} +
)} diff --git a/otel-ui/src/components/TodosVisualizer.tsx b/otel-ui/src/components/TodosVisualizer.tsx index ea2fbcb..d2ec506 100644 --- a/otel-ui/src/components/TodosVisualizer.tsx +++ b/otel-ui/src/components/TodosVisualizer.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from "react" -import { CheckCircle2, ExternalLink, ListTodo, RefreshCw, RotateCcw } from "lucide-react" +import { CheckCircle2, Crosshair, ExternalLink, ListTodo, RefreshCw, RotateCcw } from "lucide-react" import { useTodos } from "../hooks/useTodos" import { formatRelativeTime } from "../lib/timeUtils" import type { Todo } from "../lib/types" @@ -16,9 +16,20 @@ const TodoCard = ({ todo, onDone }: { todo: Todo; onDone: (todo: Todo, done: boo }`} >
-

- {todo.text} -

+
+

+ {todo.text} +

+ {todo.elementSelector && ( +
+
+ )} +
{todo.url && ( {

{todos.length === 0 && !loading && (

- Press Alt+Shift+T on a page running otel-rum.js to file one. + Press Alt+Shift+T on a page running otel-rum.js to file one — click "Reference an element" in that dialog to point at what needs to change.

)}
diff --git a/otel-ui/src/lib/types.ts b/otel-ui/src/lib/types.ts index 70cfc32..58f9b92 100644 --- a/otel-ui/src/lib/types.ts +++ b/otel-ui/src/lib/types.ts @@ -347,6 +347,8 @@ export interface Todo { url?: string; userAgent?: string; session?: string; + elementSelector?: string; + elementLabel?: string; createdAt: string; done: boolean; doneAt?: string; diff --git a/static/otel-todo.js b/static/otel-todo.js index f0f36bc..557396e 100644 --- a/static/otel-todo.js +++ b/static/otel-todo.js @@ -1,10 +1,12 @@ /** * otel-todo.js - file todos / issues straight from the page. * - * Shows a small floating symbol and listens for a keyboard shortcut. Either - * opens a native in the bottom-right corner with a text box and an - * optional component picker, and POSTs the note (with the current path and - * hash) to the collector's /v1/todos endpoint. + * Shows a small floating symbol and listens for a keyboard shortcut. Opens a + * native in the bottom-right corner with a text box, an optional + * component picker, and an "Reference an element" toggle that lets you click + * any node on the page (with a hover outline) to attach its CSS selector and + * a short label to the note. POSTs the note (with the current path, hash and + * any picked element) to the collector's /v1/todos endpoint. * *