diff --git a/.changeset/afraid-papayas-care.md b/.changeset/afraid-papayas-care.md deleted file mode 100644 index 10debc8b..00000000 --- a/.changeset/afraid-papayas-care.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@hyperdx/app": patch ---- - -feat: Group Dashboards and Searches by Tag diff --git a/.changeset/chart-explorer-auto-run.md b/.changeset/chart-explorer-auto-run.md deleted file mode 100644 index f4258994..00000000 --- a/.changeset/chart-explorer-auto-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@hyperdx/app": patch ---- - -feat: Chart Explorer now auto-executes the chart on load when a valid source is configured. Deeplinks render results without requiring a manual click. diff --git a/.changeset/cold-eyes-marry.md b/.changeset/cold-eyes-marry.md deleted file mode 100644 index 92a4c4d6..00000000 --- a/.changeset/cold-eyes-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@hyperdx/app": patch ---- - -fix: Properly enable line wrap behavior in JSON viewer by default diff --git a/.changeset/dull-grapes-whisper.md b/.changeset/dull-grapes-whisper.md deleted file mode 100644 index eb4864b4..00000000 --- a/.changeset/dull-grapes-whisper.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@hyperdx/common-utils": patch -"@hyperdx/app": patch ---- - -feat: Add dashboard template gallery diff --git a/.changeset/eighty-foxes-look.md b/.changeset/eighty-foxes-look.md deleted file mode 100644 index 75360e02..00000000 --- a/.changeset/eighty-foxes-look.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@hyperdx/app": patch ---- - -fix: differentiate map indexing vs array indexing diff --git a/.changeset/filter-panel-perf.md b/.changeset/filter-panel-perf.md deleted file mode 100644 index ccc22b4c..00000000 --- a/.changeset/filter-panel-perf.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@hyperdx/app": patch ---- - -perf: Defer expensive hooks in collapsed filter groups and virtualize nested filter lists diff --git a/.changeset/fix-slider-styling.md b/.changeset/fix-slider-styling.md deleted file mode 100644 index c9a35135..00000000 --- a/.changeset/fix-slider-styling.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@hyperdx/app": patch ---- - -fix: slider thumb and mark styling not applying theme tokens - -- Move slider thumb styling from classNames to inline styles to fix CSS specificity issue where Mantine defaults override theme tokens -- Add !important to slider mark styles to ensure token-based colors apply -- Fix vertical centering of 6px slider mark dots within the 8px track -- Remove broken translateX/translateY nudge that misaligned marks diff --git a/.changeset/gentle-numbers-warn.md b/.changeset/gentle-numbers-warn.md deleted file mode 100644 index 711f6dfa..00000000 --- a/.changeset/gentle-numbers-warn.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@hyperdx/app": patch -"@hyperdx/common-utils": patch ---- - -fix: show Map sub-fields in facet panel for non-LowCardinality value types diff --git a/.changeset/hip-goats-taste.md b/.changeset/hip-goats-taste.md deleted file mode 100644 index 1f3bf743..00000000 --- a/.changeset/hip-goats-taste.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@hyperdx/common-utils": patch -"@hyperdx/api": patch -"@hyperdx/app": patch ---- - -feat: Add favoriting for dashboards and saved searches diff --git a/.changeset/itchy-houses-cross.md b/.changeset/itchy-houses-cross.md deleted file mode 100644 index ec7ee8e2..00000000 --- a/.changeset/itchy-houses-cross.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@hyperdx/common-utils": patch -"@hyperdx/api": patch -"@hyperdx/app": patch ---- - -feat: Add more chart display units diff --git a/.changeset/lovely-radios-walk.md b/.changeset/lovely-radios-walk.md deleted file mode 100644 index 6cdcd7a5..00000000 --- a/.changeset/lovely-radios-walk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@hyperdx/app": patch ---- -fix: optimize order by should factor in wider cases, including the -default otel_traces diff --git a/.changeset/metal-radios-pay.md b/.changeset/metal-radios-pay.md deleted file mode 100644 index 1997d1a0..00000000 --- a/.changeset/metal-radios-pay.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@hyperdx/common-utils": patch -"@hyperdx/app": patch ---- - -fix: Fixed bug preventing clicking into rows with nullable date types (and other misc type) columns. diff --git a/.changeset/modern-dryers-fail.md b/.changeset/modern-dryers-fail.md deleted file mode 100644 index 00300704..00000000 --- a/.changeset/modern-dryers-fail.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@hyperdx/common-utils": minor -"@hyperdx/api": minor -"@hyperdx/app": minor ---- - -feat: new team setting for number of filters to fetch diff --git a/.changeset/moody-feet-study.md b/.changeset/moody-feet-study.md deleted file mode 100644 index fc3b0707..00000000 --- a/.changeset/moody-feet-study.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@hyperdx/common-utils": patch -"@hyperdx/app": patch ---- - -fix: render clickhouse keywords properly in codemirror diff --git a/.changeset/move-help-to-nav-links.md b/.changeset/move-help-to-nav-links.md deleted file mode 100644 index 10828fb5..00000000 --- a/.changeset/move-help-to-nav-links.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@hyperdx/app": patch ---- - -fix: move help menu from footer to main nav links diff --git a/.changeset/nice-suns-melt.md b/.changeset/nice-suns-melt.md deleted file mode 100644 index 5097ef12..00000000 --- a/.changeset/nice-suns-melt.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@hyperdx/common-utils": patch -"@hyperdx/app": patch ---- - -feat: Add $\_\_sourceTable macro diff --git a/.changeset/odd-plants-grin.md b/.changeset/odd-plants-grin.md deleted file mode 100644 index 275b56a5..00000000 --- a/.changeset/odd-plants-grin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@hyperdx/app": patch ---- - -fix: Fix query error when searching nested JSON values diff --git a/.changeset/pink-coats-leave.md b/.changeset/pink-coats-leave.md deleted file mode 100644 index 481106ed..00000000 --- a/.changeset/pink-coats-leave.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@hyperdx/common-utils": patch -"@hyperdx/app": patch ---- - -feat: Add saved searches listing page diff --git a/.changeset/rare-plants-look.md b/.changeset/rare-plants-look.md deleted file mode 100644 index 06a565cf..00000000 --- a/.changeset/rare-plants-look.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@hyperdx/app": patch ---- - -fix: Add source schema preview to SQL Charts and Trace Panel diff --git a/.changeset/replace-sidebar-collapse-icons.md b/.changeset/replace-sidebar-collapse-icons.md deleted file mode 100644 index 78144fce..00000000 --- a/.changeset/replace-sidebar-collapse-icons.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@hyperdx/app": patch ---- - -fix: replace sidebar collapse icons to align with ClickHouse collapse patterns diff --git a/.changeset/shiny-cats-design.md b/.changeset/shiny-cats-design.md deleted file mode 100644 index 33d10af7..00000000 --- a/.changeset/shiny-cats-design.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@hyperdx/app": patch ---- - -fix: Fix flaky E2E tests diff --git a/.changeset/small-mangos-bake.md b/.changeset/small-mangos-bake.md deleted file mode 100644 index 5013a536..00000000 --- a/.changeset/small-mangos-bake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@hyperdx/app": minor ---- - -feat: Add input filter pills below search input to make filters usage more clear on seach page. diff --git a/.changeset/sweet-bears-fold.md b/.changeset/sweet-bears-fold.md deleted file mode 100644 index b6359de9..00000000 --- a/.changeset/sweet-bears-fold.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@hyperdx/app": patch ---- - -feat: use 1 minute window for searches diff --git a/.changeset/tiny-forks-deny.md b/.changeset/tiny-forks-deny.md deleted file mode 100644 index a17cad18..00000000 --- a/.changeset/tiny-forks-deny.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@hyperdx/common-utils': minor -'@hyperdx/api': minor -'@hyperdx/app': minor ---- - -feat: support sample-weighted aggregations for sampled trace data diff --git a/.env b/.env index afcd2de1..7d0a5bb5 100644 --- a/.env +++ b/.env @@ -8,8 +8,8 @@ NEXT_ALL_IN_ONE_IMAGE_NAME_DOCKERHUB=clickhouse/clickstack-all-in-one ALL_IN_ONE_IMAGE_NAME_DOCKERHUB=hyperdx/hyperdx-all-in-one NEXT_OTEL_COLLECTOR_IMAGE_NAME_DOCKERHUB=clickhouse/clickstack-otel-collector OTEL_COLLECTOR_IMAGE_NAME_DOCKERHUB=hyperdx/hyperdx-otel-collector -CODE_VERSION=2.22.1 -IMAGE_VERSION_SUB_TAG=.22.1 +CODE_VERSION=2.23.0 +IMAGE_VERSION_SUB_TAG=.23.0 IMAGE_VERSION=2 IMAGE_NIGHTLY_TAG=2-nightly IMAGE_LATEST_TAG=latest diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 8a6a228f..9acf4a84 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -47,6 +47,19 @@ jobs: - name: Start E2E Docker Compose run: | + # Pre-pull images with retries to handle transient Docker Hub timeouts + for attempt in 1 2 3; do + if docker compose -p e2e-0 -f packages/app/tests/e2e/docker-compose.yml pull; then + echo "Docker images pulled successfully on attempt $attempt" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "Failed to pull Docker images after 3 attempts" + exit 1 + fi + echo "Docker pull failed (attempt $attempt/3), retrying in 10s..." + sleep 10 + done docker compose -p e2e-0 -f packages/app/tests/e2e/docker-compose.yml up -d echo "Waiting for MongoDB..." for i in $(seq 1 30); do diff --git a/.github/workflows/pr-triage.yml b/.github/workflows/pr-triage.yml new file mode 100644 index 00000000..3f697080 --- /dev/null +++ b/.github/workflows/pr-triage.yml @@ -0,0 +1,261 @@ +name: PR Triage + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + workflow_dispatch: + inputs: + pr_number: + description: + PR number to classify (leave blank to classify all open PRs) + required: false + type: string + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + classify: + name: Classify PR risk tier + runs-on: ubuntu-24.04 + # For pull_request events skip drafts; workflow_dispatch always runs + if: + ${{ github.event_name == 'workflow_dispatch' || + !github.event.pull_request.draft }} + steps: + - name: Classify and label PR(s) + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + + // ── Determine which PRs to process ────────────────────────────── + let prNumbers; + if (context.eventName === 'workflow_dispatch') { + // Use context.payload.inputs to avoid script-injection via template interpolation + const input = (context.payload.inputs?.pr_number ?? '').trim(); + if (input && input !== '') { + prNumbers = [Number(input)]; + } else { + const openPRs = await github.paginate( + github.rest.pulls.list, + { owner, repo, state: 'open', per_page: 100 } + ); + prNumbers = openPRs.map(pr => pr.number); + console.log(`Bulk triage: found ${prNumbers.length} open PRs`); + } + } else { + prNumbers = [context.payload.pull_request.number]; + } + + // ── Shared constants ───────────────────────────────────────────── + const TIER4_PATTERNS = [ + /^packages\/api\/src\/middleware\/auth/, + /^packages\/api\/src\/routers\/api\/me\./, + /^packages\/api\/src\/routers\/api\/team\./, + /^packages\/api\/src\/routers\/external-api\//, + /^packages\/api\/src\/models\/(user|team|teamInvite)\./, + /^packages\/api\/src\/config\./, + /^packages\/api\/src\/tasks\//, + /^packages\/otel-collector\//, + /^docker\/otel-collector\//, + /^docker\/clickhouse\//, + /^\.github\/workflows\//, + ]; + + const TIER1_PATTERNS = [ + /\.(md|txt|png|jpg|jpeg|gif|svg|ico)$/i, + /^yarn\.lock$/, + /^package-lock\.json$/, + /^\.yarnrc\.yml$/, + /^\.github\/images\//, + /^\.env\.example$/, + ]; + + const BOT_AUTHORS = ['dependabot', 'dependabot[bot]']; + const AGENT_BRANCH_PREFIXES = ['claude/', 'agent/', 'ai/']; + + const TIER_LABELS = { + 1: { name: 'review/tier-1', color: '0E8A16', description: 'Trivial — auto-merge candidate once CI passes' }, + 2: { name: 'review/tier-2', color: '1D76DB', description: 'Low risk — AI review + quick human skim' }, + 3: { name: 'review/tier-3', color: 'E4E669', description: 'Standard — full human review required' }, + 4: { name: 'review/tier-4', color: 'B60205', description: 'Critical — deep review + domain expert sign-off' }, + }; + + const TIER_INFO = { + 1: { + emoji: '🟢', + headline: 'Tier 1 — Trivial', + detail: 'Docs, images, lock files, or a dependency bump. No functional code changes detected.', + process: 'Auto-merge once CI passes. No human review required.', + sla: 'Resolves automatically.', + }, + 2: { + emoji: '🔵', + headline: 'Tier 2 — Low Risk', + detail: 'Small, isolated change with no API route or data model modifications.', + process: 'AI review + quick human skim (target: 5–15 min). Reviewer validates AI assessment and checks for domain-specific concerns.', + sla: 'Resolve within 4 business hours.', + }, + 3: { + emoji: '🟡', + headline: 'Tier 3 — Standard', + detail: 'Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.', + process: 'Full human review — logic, architecture, edge cases.', + sla: 'First-pass feedback within 1 business day.', + }, + 4: { + emoji: '🔴', + headline: 'Tier 4 — Critical', + detail: 'Touches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD.', + process: 'Deep review from a domain expert. Synchronous walkthrough may be required.', + sla: 'Schedule synchronous review within 2 business days.', + }, + }; + + // ── Ensure tier labels exist (once, before the loop) ───────────── + const repoLabels = await github.paginate( + github.rest.issues.listLabelsForRepo, + { owner, repo, per_page: 100 } + ); + const repoLabelNames = new Set(repoLabels.map(l => l.name)); + for (const label of Object.values(TIER_LABELS)) { + if (!repoLabelNames.has(label.name)) { + await github.rest.issues.createLabel({ owner, repo, ...label }); + repoLabelNames.add(label.name); + } + } + + // ── Classify a single PR ───────────────────────────────────────── + async function classifyPR(prNumber) { + // Fetch changed files + const filesRes = await github.paginate( + github.rest.pulls.listFiles, + { owner, repo, pull_number: prNumber, per_page: 100 } + ); + const files = filesRes.map(f => f.filename); + const linesChanged = filesRes.reduce((sum, f) => sum + f.additions + f.deletions, 0); + + // Fetch PR metadata + const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber }); + const author = pr.user.login; + const branchName = pr.head.ref; + + // Skip drafts when running in bulk mode + if (pr.draft) { + console.log(`Skipping PR #${prNumber}: draft`); + return; + } + + // Check for manual tier override — if a human last applied the label, respect it + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ owner, repo, issue_number: prNumber }); + const existingTierLabel = currentLabels.find(l => l.name.startsWith('review/tier-')); + if (existingTierLabel) { + const events = await github.paginate( + github.rest.issues.listEvents, + { owner, repo, issue_number: prNumber, per_page: 100 } + ); + const lastLabelEvent = events + .filter(e => e.event === 'labeled' && e.label?.name === existingTierLabel.name) + .pop(); + if (lastLabelEvent && lastLabelEvent.actor.type !== 'Bot') { + console.log(`PR #${prNumber}: tier manually set to ${existingTierLabel.name} by ${lastLabelEvent.actor.login} — skipping`); + return; + } + } + + // Classify + const isTier4 = files.some(f => TIER4_PATTERNS.some(p => p.test(f))); + const isTrivialAuthor = BOT_AUTHORS.includes(author); + const allFilesTrivial = files.length > 0 && files.every(f => TIER1_PATTERNS.some(p => p.test(f))); + const isTier1 = isTrivialAuthor || allFilesTrivial; + const isAgentBranch = AGENT_BRANCH_PREFIXES.some(p => branchName.startsWith(p)); + const touchesApiModels = files.some(f => + f.startsWith('packages/api/src/models/') || f.startsWith('packages/api/src/routers/') + ); + const isSmallDiff = linesChanged < 100; + // Agent branches are bumped to Tier 3 regardless of size to ensure human review + const isTier2 = !isTier4 && !isTier1 && isSmallDiff && !touchesApiModels && !isAgentBranch; + + let tier; + if (isTier4) tier = 4; + else if (isTier1) tier = 1; + else if (isTier2) tier = 2; + else tier = 3; + + // Escalate very large non-critical PRs to Tier 4; this also applies to agent + // branches that were bumped to Tier 3 above — a 400+ line agent-generated change + // warrants deep review regardless of which files it touches. + if (tier === 3 && linesChanged > 400) tier = 4; + + // Apply label + for (const existing of currentLabels) { + if (existing.name.startsWith('review/tier-') && existing.name !== TIER_LABELS[tier].name) { + await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: existing.name }); + } + } + if (!currentLabels.find(l => l.name === TIER_LABELS[tier].name)) { + await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: [TIER_LABELS[tier].name] }); + } + + // Build comment body + const info = TIER_INFO[tier]; + const signals = []; + if (isTier4) signals.push('critical-path files detected'); + if (isAgentBranch) signals.push(`agent branch (\`${branchName}\`)`); + if (linesChanged > 400) signals.push(`large diff (${linesChanged} lines changed)`); + if (isTrivialAuthor) signals.push(`bot author (${author})`); + if (allFilesTrivial && !isTrivialAuthor) signals.push('all files are docs/images/lock files'); + if (touchesApiModels) signals.push('API routes or data models changed'); + + const signalList = signals.length > 0 ? `\n**Signals**: ${signals.join(', ')}` : ''; + + const body = [ + '', + `## ${info.emoji} ${info.headline}`, + '', + info.detail, + signalList, + '', + `**Review process**: ${info.process}`, + `**SLA**: ${info.sla}`, + '', + `
Stats`, + '', + `- Files changed: ${files.length}`, + `- Lines changed: ${linesChanged}`, + `- Branch: \`${branchName}\``, + `- Author: ${author}`, + '', + '
', + '', + `> To override this classification, remove the \`${TIER_LABELS[tier].name}\` label and apply a different \`review/tier-*\` label. Manual overrides are preserved on subsequent pushes.`, + ].join('\n'); + + // Post or update the single triage comment + const comments = await github.paginate( + github.rest.issues.listComments, + { owner, repo, issue_number: prNumber, per_page: 100 } + ); + const existing = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.includes('')); + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body }); + } + + console.log(`PR #${prNumber}: Tier ${tier} (${linesChanged} lines, ${files.length} files)`); + } + + // ── Process all target PRs ─────────────────────────────────────── + for (const prNumber of prNumbers) { + try { + await classifyPR(prNumber); + } catch (err) { + console.error(`PR #${prNumber}: classification failed — ${err.message}`); + } + } diff --git a/AGENTS.md b/AGENTS.md index e6b2f59d..9b32d055 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -160,6 +160,27 @@ Before writing new E2E tests or reviewing a PR that adds them: - **Database patterns**: MongoDB for metadata with Mongoose, ClickHouse for telemetry queries +## PR Hygiene for Agent-Generated Code + +When using agentic tools to generate PRs, follow these practices to keep reviews +efficient and accurate: + +1. **Scope PRs to a single logical change**, even if the agent can produce more + in one session. Smaller, focused PRs move through the review pipeline faster + and are easier to classify accurately. + +2. **Write the PR description to explain intent (the "why"), not just what + changed.** Reviewers need to understand the goal to catch cases where the + agent solved the wrong problem or made a plausible-but-wrong trade-off. + +3. **Name agent-generated branches with a `claude/`, `agent/`, or `ai/` prefix** + (e.g., `claude/add-rate-limiting`). This allows the PR triage classifier to + apply appropriate scrutiny and lets reviewers calibrate their attention. + +4. **Write or update tests alongside the implementation**, not after. Configure + your agent to produce tests before writing implementation code. See the + Testing section below for the commands to use. + ## GitHub Action Workflow (when invoked via @claude) When working on issues or PRs through the GitHub Action: diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 4a325f3f..65a1ff7b 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,29 @@ # @hyperdx/api +## 2.23.0 + +### Minor Changes + +- a15122b3: feat: new team setting for number of filters to fetch +- 941d0450: feat: support sample-weighted aggregations for sampled trace data + +### Patch Changes + +- 53ba1e39: feat: Add favoriting for dashboards and saved searches +- b7581db8: feat: Add more chart display units +- 59b1f46f: fix: Show alerts on a tile only when dashboard matches +- Updated dependencies [518bda7d] +- Updated dependencies [4e54d850] +- Updated dependencies [53ba1e39] +- Updated dependencies [b7581db8] +- Updated dependencies [48a8d32b] +- Updated dependencies [a15122b3] +- Updated dependencies [a55b151e] +- Updated dependencies [308da30b] +- Updated dependencies [e5c7fdf9] +- Updated dependencies [941d0450] + - @hyperdx/common-utils@0.17.0 + ## 2.22.1 ### Patch Changes diff --git a/packages/api/package.json b/packages/api/package.json index db95bf59..23dc3d1b 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@hyperdx/api", - "version": "2.22.1", + "version": "2.23.0", "license": "MIT", "private": true, "engines": { @@ -10,7 +10,7 @@ "@ai-sdk/anthropic": "^3.0.58", "@ai-sdk/openai": "^3.0.47", "@esm2cjs/p-queue": "^7.3.0", - "@hyperdx/common-utils": "^0.16.2", + "@hyperdx/common-utils": "^0.17.0", "@hyperdx/node-opentelemetry": "^0.9.0", "@hyperdx/passport-local-mongoose": "^9.0.1", "@opentelemetry/api": "^1.8.0", diff --git a/packages/api/src/controllers/alerts.ts b/packages/api/src/controllers/alerts.ts index 90512a5f..c5ef1fc3 100644 --- a/packages/api/src/controllers/alerts.ts +++ b/packages/api/src/controllers/alerts.ts @@ -203,12 +203,14 @@ export const getAlertById = async ( }); }; -export const getTeamDashboardAlertsByTile = async (teamId: ObjectId) => { +export const getTeamDashboardAlertsByDashboardAndTile = async ( + teamId: ObjectId, +) => { const alerts = await Alert.find({ source: AlertSource.TILE, team: teamId, }).populate('createdBy', 'email name'); - return groupBy(alerts, 'tileId'); + return groupBy(alerts, a => `${a.dashboard?.toString()}:${a.tileId}`); }; export const getDashboardAlertsByTile = async ( diff --git a/packages/api/src/controllers/dashboard.ts b/packages/api/src/controllers/dashboard.ts index d56163f2..4449ad15 100644 --- a/packages/api/src/controllers/dashboard.ts +++ b/packages/api/src/controllers/dashboard.ts @@ -11,7 +11,7 @@ import { createOrUpdateDashboardAlerts, deleteDashboardAlerts, getDashboardAlertsByTile, - getTeamDashboardAlertsByTile, + getTeamDashboardAlertsByDashboardAndTile, } from '@/controllers/alerts'; import type { ObjectId } from '@/models'; import type { AlertDocument, IAlert } from '@/models/alert'; @@ -96,7 +96,7 @@ async function syncDashboardAlerts( export async function getDashboards(teamId: ObjectId) { const [_dashboards, alerts] = await Promise.all([ Dashboard.find({ team: teamId }), - getTeamDashboardAlertsByTile(teamId), + getTeamDashboardAlertsByDashboardAndTile(teamId), ]); const dashboards = _dashboards @@ -105,7 +105,10 @@ export async function getDashboards(teamId: ObjectId) { ...d, tiles: d.tiles.map(t => ({ ...t, - config: { ...t.config, alert: alerts[t.id]?.[0] }, + config: { + ...t.config, + alert: alerts[`${d._id.toString()}:${t.id}`]?.[0], + }, })), })); diff --git a/packages/api/src/routers/api/__tests__/dashboard.test.ts b/packages/api/src/routers/api/__tests__/dashboard.test.ts index baaacc53..04c20f2e 100644 --- a/packages/api/src/routers/api/__tests__/dashboard.test.ts +++ b/packages/api/src/routers/api/__tests__/dashboard.test.ts @@ -341,6 +341,48 @@ describe('dashboard router', () => { expect(allTilesPostDelete).toEqual(alertsPostDeleteTiles); }); + it('alert on a tile only appears on the dashboard that owns it, not on another dashboard with the same tile ID', async () => { + const sharedTileId = new mongoose.Types.ObjectId().toHexString(); + const mockAlert = makeMockAlert(webhook._id.toString()); + + // Create dashboard A with an alert on the tile + const dashboardA = await agent + .post('/dashboards') + .send({ + name: 'Dashboard A', + tiles: [makeTile({ id: sharedTileId, alert: mockAlert })], + tags: [], + }) + .expect(200); + + // Create dashboard B with a tile that has the same ID, but no alert + const dashboardB = await agent + .post('/dashboards') + .send({ + name: 'Dashboard B', + tiles: [makeTile({ id: sharedTileId })], + tags: [], + }) + .expect(200); + + // Fetch all dashboards + const dashboards = await agent.get('/dashboards').expect(200); + + const fetchedA = dashboards.body.find( + (d: any) => d._id === dashboardA.body.id, + ); + const fetchedB = dashboards.body.find( + (d: any) => d._id === dashboardB.body.id, + ); + + // The alert should appear on dashboard A's tile + expect(fetchedA.tiles[0].config.alert).toBeTruthy(); + expect(fetchedA.tiles[0].config.alert.tileId).toBe(sharedTileId); + + // The alert should NOT appear on dashboard B's tile + expect(fetchedB.tiles[0].config.alert).toBeUndefined(); + }); + it('preserves alert creator when different user updates dashboard', async () => { const mockAlert = makeMockAlert(webhook._id.toString()); const currentUser = user; diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 43a2fd6a..92b4fd34 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,58 @@ # @hyperdx/app +## 2.23.0 + +### Minor Changes + +- a15122b3: feat: new team setting for number of filters to fetch +- 20e47207: feat: Add input filter pills below search input to make filters usage more clear on seach page. +- 941d0450: feat: support sample-weighted aggregations for sampled trace data + +### Patch Changes + +- bfc93811: feat: Group Dashboards and Searches by Tag +- 859ced5c: feat: Chart Explorer now auto-executes the chart on load when a valid source is configured. Deeplinks render results without requiring a manual click. +- e6a0455a: fix: Properly enable line wrap behavior in JSON viewer by default +- 518bda7d: feat: Add dashboard template gallery +- 676e4f4b: fix: differentiate map indexing vs array indexing +- 9852e9b0: perf: Defer expensive hooks in collapsed filter groups and virtualize nested filter lists +- 5e5c6a94: fix: slider thumb and mark styling not applying theme tokens + + - Move slider thumb styling from classNames to inline styles to fix CSS specificity issue where Mantine defaults override theme tokens + - Add !important to slider mark styles to ensure token-based colors apply + - Fix vertical centering of 6px slider mark dots within the 8px track + - Remove broken translateX/translateY nudge that misaligned marks + +- 4e54d850: fix: show Map sub-fields in facet panel for non-LowCardinality value types +- 011a245f: fix: Fix error state and table overflows +- 53ba1e39: feat: Add favoriting for dashboards and saved searches +- b7581db8: feat: Add more chart display units +- 05a1b765: fix: optimize order by should factor in wider cases, including the + default otel_traces +- 48a8d32b: fix: Fixed bug preventing clicking into rows with nullable date types (and other misc type) columns. +- a55b151e: fix: render clickhouse keywords properly in codemirror +- 9cfb7e9c: fix: move help menu from footer to main nav links +- 308da30b: feat: Add $\_\_sourceTable macro +- 2bb8ccdc: fix: Fix query error when searching nested JSON values +- df170d1e: fix: Show error on DBInfraPanel when correlated metric source is missing +- e5c7fdf9: feat: Add saved searches listing page +- 0cc1295d: fix: Add source schema preview to SQL Charts and Trace Panel +- 1b77eab9: fix: replace sidebar collapse icons to align with ClickHouse collapse patterns +- 853da16a: fix: Fix flaky E2E tests +- b4e1498e: fix: Fix minor bugs in chart editor +- bb24994f: feat: use 1 minute window for searches +- Updated dependencies [518bda7d] +- Updated dependencies [4e54d850] +- Updated dependencies [53ba1e39] +- Updated dependencies [b7581db8] +- Updated dependencies [48a8d32b] +- Updated dependencies [a15122b3] +- Updated dependencies [a55b151e] +- Updated dependencies [308da30b] +- Updated dependencies [e5c7fdf9] +- Updated dependencies [941d0450] + - @hyperdx/common-utils@0.17.0 + ## 2.22.1 ### Patch Changes diff --git a/packages/app/eslint.config.mjs b/packages/app/eslint.config.mjs index e4c7f05a..73977dc5 100644 --- a/packages/app/eslint.config.mjs +++ b/packages/app/eslint.config.mjs @@ -2,7 +2,7 @@ import js from '@eslint/js'; import tseslint from 'typescript-eslint'; import storybook from 'eslint-plugin-storybook'; import nextPlugin from '@next/eslint-plugin-next'; -import reactPlugin from 'eslint-plugin-react'; +import eslintReactPlugin from '@eslint-react/eslint-plugin'; import reactHooksPlugin from 'eslint-plugin-react-hooks'; import prettierConfig from 'eslint-config-prettier'; import simpleImportSort from 'eslint-plugin-simple-import-sort'; @@ -112,16 +112,19 @@ export default [ files: ['**/*.{js,jsx,ts,tsx}'], plugins: { '@next/next': nextPlugin, - react: reactPlugin, 'react-hooks': reactHooksPlugin, 'simple-import-sort': simpleImportSort, 'react-hook-form': fixupPluginRules(reactHookFormPlugin), // not compatible with eslint 9 yet + ...eslintReactPlugin.configs.recommended.plugins, }, rules: { ...nextPlugin.configs.recommended.rules, ...nextPlugin.configs['core-web-vitals'].rules, ...reactHooksPlugin.configs.recommended.rules, 'react-hooks/set-state-in-effect': 'warn', + 'react-hooks/exhaustive-deps': 'error', + 'react-hook-form/no-use-watch': 'error', + '@eslint-react/no-unstable-default-props': 'error', '@typescript-eslint/ban-ts-comment': 'warn', '@typescript-eslint/no-empty-function': 'warn', '@typescript-eslint/no-explicit-any': 'off', @@ -135,7 +138,6 @@ export default [ varsIgnorePattern: '^_', }, ], - 'react/display-name': 'off', 'simple-import-sort/exports': 'error', 'simple-import-sort/imports': [ 'error', @@ -155,9 +157,7 @@ export default [ ...UI_SYNTAX_RESTRICTIONS, ...DATE_SYNTAX_RESTRICTIONS, ], - 'react-hooks/exhaustive-deps': 'error', 'no-console': ['error', { allow: ['warn', 'error'] }], - 'react-hook-form/no-use-watch': 'error', }, languageOptions: { parser: tseslint.parser, diff --git a/packages/app/package.json b/packages/app/package.json index 4aeb6ee6..1710c36a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@hyperdx/app", - "version": "2.22.1", + "version": "2.23.0", "private": true, "license": "MIT", "engines": { @@ -34,7 +34,7 @@ "@dagrejs/dagre": "^1.1.5", "@hookform/resolvers": "^3.9.0", "@hyperdx/browser": "^0.22.0", - "@hyperdx/common-utils": "^0.16.2", + "@hyperdx/common-utils": "^0.17.0", "@hyperdx/node-opentelemetry": "^0.9.0", "@mantine/core": "^7.17.8", "@mantine/dates": "^7.17.8", @@ -100,6 +100,7 @@ }, "devDependencies": { "@chromatic-com/storybook": "^4.1.3", + "@eslint-react/eslint-plugin": "^3.0.0", "@eslint/compat": "^2.0.0", "@jedmao/location": "^3.0.0", "@next/eslint-plugin-next": "^16.0.10", @@ -129,7 +130,6 @@ "@types/sqlstring": "^2.3.2", "eslint-config-next": "^16.0.10", "eslint-plugin-playwright": "^2.4.0", - "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hook-form": "^0.3.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-storybook": "10.1.4", diff --git a/packages/app/src/ClickhousePage.tsx b/packages/app/src/ClickhousePage.tsx index 0fc5a28e..3c529341 100644 --- a/packages/app/src/ClickhousePage.tsx +++ b/packages/app/src/ClickhousePage.tsx @@ -784,7 +784,12 @@ function ClickhousePage() { - + Slowest Queries diff --git a/packages/app/src/DBDashboardPage.tsx b/packages/app/src/DBDashboardPage.tsx index ca028bef..a28f8a27 100644 --- a/packages/app/src/DBDashboardPage.tsx +++ b/packages/app/src/DBDashboardPage.tsx @@ -367,27 +367,28 @@ const Tile = forwardRef( > {(chart.config.displayType === DisplayType.Line || chart.config.displayType === DisplayType.StackedBar || - chart.config.displayType === DisplayType.Number) && ( - +} - mr={4} - > - - - - - - - )} + chart.config.displayType === DisplayType.Number) && + !isRawSqlSavedChartConfig(chart.config) && ( + +} + mr={4} + > + + + + + + + )} - + )} diff --git a/packages/app/src/HDXMultiSeriesTimeChart.tsx b/packages/app/src/HDXMultiSeriesTimeChart.tsx index a35a98d0..8d1f11bb 100644 --- a/packages/app/src/HDXMultiSeriesTimeChart.tsx +++ b/packages/app/src/HDXMultiSeriesTimeChart.tsx @@ -217,19 +217,14 @@ const LegendRenderer = memo<{ selectedSeries?: Set; onToggleSeries?: (seriesName: string, isShiftKey?: boolean) => void; }>(props => { - const { - payload = [], - lineDataMap, - allLineData = [], - selectedSeries = new Set(), - onToggleSeries, - } = props; + const { payload, lineDataMap, allLineData, selectedSeries, onToggleSeries } = + props; - const hasSelection = selectedSeries.size > 0; + const hasSelection = !!selectedSeries && selectedSeries.size > 0; // Use allLineData to ensure all series are always shown in legend const allSeriesPayload = useMemo(() => { - if (allLineData.length > 0) { + if (allLineData?.length) { return allLineData.map(ld => ({ dataKey: ld.dataKey, value: ld.displayName || ld.dataKey, @@ -237,7 +232,7 @@ const LegendRenderer = memo<{ payload: { strokeDasharray: ld.isDashed ? '4 3' : '0' }, })); } - return payload; + return payload ?? []; }, [allLineData, payload]); const sortedLegendItems = useMemo(() => { @@ -268,7 +263,7 @@ const LegendRenderer = memo<{ return (
{shownItems.map((entry, index) => { - const isSelected = selectedSeries.has(entry.value); + const isSelected = !!selectedSeries?.has(entry.value); const isDisabled = hasSelection && !isSelected; return (
{restItems.map((entry, index) => { - const isSelected = selectedSeries.has(entry.value); + const isSelected = !!selectedSeries?.has(entry.value); const isDisabled = hasSelection && !isSelected; return ( void; + hideCustom?: boolean; }) { const _onChange = useCallback( (value: string | null) => { @@ -42,7 +44,7 @@ function AggFnSelect({ value={value} defaultValue={defaultValue} onChange={_onChange} - data={AGG_FNS} + data={hideCustom ? AGG_FNS.filter(fn => fn.value !== 'none') : AGG_FNS} data-testid="agg-fn-select" /> ); @@ -52,11 +54,13 @@ export function AggFnSelectControlled({ aggFnName, quantileLevelName, defaultValue, + hideCustom, ...props }: { defaultValue: string; aggFnName: string; quantileLevelName: string; + hideCustom?: boolean; } & Omit, 'name'>) { const { field: { onChange: onAggFnChange, value: aggFnValue }, @@ -96,6 +100,7 @@ export function AggFnSelectControlled({ value={value} defaultValue={defaultValue} onChange={onChange} + hideCustom={hideCustom} /> ); } diff --git a/packages/app/src/components/ContextSidePanel.tsx b/packages/app/src/components/ContextSidePanel.tsx index e71ba620..3f7fae36 100644 --- a/packages/app/src/components/ContextSidePanel.tsx +++ b/packages/app/src/components/ContextSidePanel.tsx @@ -79,7 +79,7 @@ export default function ContextSubpanel({ dbSqlRowTableConfig, rowData, rowId, - breadcrumbPath = [], + breadcrumbPath, onBreadcrumbClick, }: ContextSubpanelProps) { const QUERY_KEY_PREFIX = 'context'; @@ -102,7 +102,7 @@ export default function ContextSubpanel({ const [debouncedWhere] = useDebouncedValue(formWhere, 1000); // State management for nested panels - const isNested = breadcrumbPath.length > 0; + const isNested = !!breadcrumbPath?.length; const { contextRowId, @@ -327,7 +327,7 @@ export default function ContextSubpanel({ onClose={handleContextSidePanelClose} isNestedPanel={true} breadcrumbPath={[ - ...breadcrumbPath, + ...(breadcrumbPath ?? []), { label: `Surrounding Context`, rowData, diff --git a/packages/app/src/components/DBEditTimeChartForm.tsx b/packages/app/src/components/DBEditTimeChartForm.tsx index 545c87d1..daed5a92 100644 --- a/packages/app/src/components/DBEditTimeChartForm.tsx +++ b/packages/app/src/components/DBEditTimeChartForm.tsx @@ -220,11 +220,17 @@ function ChartSeriesEditorComponent({ const metricType = useWatch({ control, name: `${namePrefix}metricType` }); // Initialize metricType to 'gauge' when switching to a metric source + // and reset 'custom' aggFn to 'count' since custom is not supported for metrics useEffect(() => { - if (tableSource?.kind === SourceKind.Metric && !metricType) { - setValue(`${namePrefix}metricType`, MetricsDataType.Gauge); + if (tableSource?.kind === SourceKind.Metric) { + if (!metricType) { + setValue(`${namePrefix}metricType`, MetricsDataType.Gauge); + } + if (aggFn === 'none') { + setValue(`${namePrefix}aggFn`, 'count'); + } } - }, [tableSource?.kind, metricType, namePrefix, setValue]); + }, [tableSource?.kind, metricType, aggFn, namePrefix, setValue]); const tableName = tableSource?.kind === SourceKind.Metric @@ -363,6 +369,7 @@ function ChartSeriesEditorComponent({ quantileLevelName={`${namePrefix}level`} defaultValue={AGG_FNS[0]?.value ?? 'avg'} control={control} + hideCustom={tableSource?.kind === SourceKind.Metric} />
{tableSource?.kind === SourceKind.Metric && metricType && ( @@ -496,7 +503,7 @@ function ChartSeriesEditorComponent({ language={aggConditionLanguage === 'sql' ? 'sql' : 'lucene'} metricMetadata={metricMetadata} onAddToWhere={handleAddToWhere} - onAddToGroupBy={handleAddToGroupBy} + onAddToGroupBy={showGroupBy ? handleAddToGroupBy : undefined} /> )} diff --git a/packages/app/src/components/DBHighlightedAttributesList.tsx b/packages/app/src/components/DBHighlightedAttributesList.tsx index 603597fa..a32bd49c 100644 --- a/packages/app/src/components/DBHighlightedAttributesList.tsx +++ b/packages/app/src/components/DBHighlightedAttributesList.tsx @@ -16,7 +16,7 @@ export type HighlightedAttribute = { }; export function DBHighlightedAttributesList({ - attributes = [], + attributes, }: { attributes: HighlightedAttribute[]; }) { diff --git a/packages/app/src/components/DBInfraPanel.tsx b/packages/app/src/components/DBInfraPanel.tsx index 3f049d90..be434f6f 100644 --- a/packages/app/src/components/DBInfraPanel.tsx +++ b/packages/app/src/components/DBInfraPanel.tsx @@ -1,4 +1,5 @@ import { useMemo, useState } from 'react'; +import Link from 'next/link'; import { add, min, sub } from 'date-fns'; import { convertDateRangeToGranularityString, @@ -12,16 +13,23 @@ import { TSource, } from '@hyperdx/common-utils/dist/types'; import { + Alert, + Anchor, Box, Card, Group, + Modal, ScrollArea, SegmentedControl, SimpleGrid, Stack, + Text, } from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; import { convertV1ChartConfigToV2 } from '@/ChartUtils'; +import { TableSourceForm } from '@/components/Sources/SourceForm'; +import { IS_LOCAL_MODE } from '@/config'; import { useSource } from '@/source'; import { @@ -211,11 +219,14 @@ export default ({ rowId: string | undefined | null; source: TSource; }) => { + const [editModalOpened, { open: openEditModal, close: closeEditModal }] = + useDisclosure(false); + const metricSourceId = isLogSource(source) || isTraceSource(source) ? source.metricSourceId : undefined; - const { data: metricSource } = useSource({ + const { data: metricSource, isLoading: isLoadingMetricSource } = useSource({ id: metricSourceId, kinds: [SourceKind.Metric], }); @@ -227,6 +238,39 @@ export default ({ return ( + {!metricSource && !isLoadingMetricSource && ( + <> + + + {metricSourceId + ? `The correlated metric source for "${source.name}" could not be found.` + : `Source "${source.name}" does not have a correlated metric source.`}{' '} + Infrastructure metrics can be displayed when a metric source is + configured in{' '} + {IS_LOCAL_MODE ? ( + + Source Settings + + ) : ( + + Team Settings + + )} + . + + + {IS_LOCAL_MODE && ( + + + + )} + + )} {podUid && (
{metricSource && ( diff --git a/packages/app/src/components/DBListBarChart.tsx b/packages/app/src/components/DBListBarChart.tsx index b33caae1..6c477d71 100644 --- a/packages/app/src/components/DBListBarChart.tsx +++ b/packages/app/src/components/DBListBarChart.tsx @@ -180,7 +180,7 @@ export default function DBListBarChart({ enabled, valueColumn, groupColumn, - hiddenSeries = [], + hiddenSeries, title, toolbarItems, showMVOptimizationIndicator = true, @@ -220,7 +220,7 @@ export default function DBListBarChart({ } return Object.keys(rows?.[0]) - .filter(key => !hiddenSeries.includes(key)) + .filter(key => !hiddenSeries?.includes(key)) .map(key => ({ dataKey: key, displayName: key, diff --git a/packages/app/src/components/DBRowJsonViewer.tsx b/packages/app/src/components/DBRowJsonViewer.tsx index eab6553e..f1eac9fa 100644 --- a/packages/app/src/components/DBRowJsonViewer.tsx +++ b/packages/app/src/components/DBRowJsonViewer.tsx @@ -320,7 +320,7 @@ function HyperJsonMenu({ rowData }: { rowData: any }) { export function DBRowJsonViewer({ data, - jsonColumns = [], + jsonColumns, }: { data: any; jsonColumns?: string[]; diff --git a/packages/app/src/components/DBRowSidePanel.tsx b/packages/app/src/components/DBRowSidePanel.tsx index 7bd184c1..adf027db 100644 --- a/packages/app/src/components/DBRowSidePanel.tsx +++ b/packages/app/src/components/DBRowSidePanel.tsx @@ -107,7 +107,7 @@ const DBRowSidePanel = ({ isNestedPanel = false, setSubDrawerOpen, onClose, - breadcrumbPath = [], + breadcrumbPath, onBreadcrumbClick, }: DBRowSidePanelProps & { setSubDrawerOpen: Dispatch>; @@ -127,7 +127,7 @@ const DBRowSidePanel = ({ const handleBreadcrumbClick = useCallback( (targetLevel: number) => { // Current panel's level in the hierarchy - const currentLevel = breadcrumbPath.length; + const currentLevel = breadcrumbPath?.length ?? 0; // The target panel level corresponds to the breadcrumb index: // - targetLevel 0 = root panel (breadcrumbPath.length = 0) @@ -149,7 +149,7 @@ const DBRowSidePanel = ({ onBreadcrumbClick?.(targetLevel); } }, - [breadcrumbPath.length, onBreadcrumbClick, onClose], + [breadcrumbPath?.length, onBreadcrumbClick, onClose], ); const hasOverviewPanel = useMemo(() => { @@ -551,7 +551,7 @@ export default function DBRowSidePanelErrorBoundary({ aliasWith, source, isNestedPanel, - breadcrumbPath = [], + breadcrumbPath, onBreadcrumbClick, }: DBRowSidePanelProps) { const contextZIndex = useZIndex(); diff --git a/packages/app/src/components/DBRowSidePanelHeader.tsx b/packages/app/src/components/DBRowSidePanelHeader.tsx index f5786eb7..64dea296 100644 --- a/packages/app/src/components/DBRowSidePanelHeader.tsx +++ b/packages/app/src/components/DBRowSidePanelHeader.tsx @@ -127,13 +127,13 @@ function BreadcrumbNavigation({ } export default function DBRowSidePanelHeader({ - attributes = [], + attributes, mainContent = '', mainContentHeader, date, severityText, rowData, - breadcrumbPath = [], + breadcrumbPath, onBreadcrumbClick, }: { date: Date; @@ -197,11 +197,19 @@ export default function DBRowSidePanelHeader({ [generateSearchUrl], ); + const breadCrumbPathWithDefault = useMemo(() => { + return breadcrumbPath ?? []; + }, [breadcrumbPath]); + + const attributesWithDefault = useMemo(() => { + return attributes ?? []; + }, [attributes]); + return ( <> {/* Breadcrumb navigation */} @@ -277,7 +285,7 @@ export default function DBRowSidePanelHeader({ )} - + ); diff --git a/packages/app/src/components/DBSearchPageFilters.tsx b/packages/app/src/components/DBSearchPageFilters.tsx index 0964c7ef..a8fb37f1 100644 --- a/packages/app/src/components/DBSearchPageFilters.tsx +++ b/packages/app/src/components/DBSearchPageFilters.tsx @@ -330,15 +330,17 @@ const FilterRangeDisplay = ({ ); }; +type SelectedValues = { + included: Set; + excluded: Set; + range?: { min: number; max: number }; +}; + export type FilterGroupProps = { name: string; options: { value: string | boolean; label: string }[]; optionsLoading?: boolean; - selectedValues?: { - included: Set; - excluded: Set; - range?: { min: number; max: number }; - }; + selectedValues?: SelectedValues; onChange: (value: string | boolean) => void; onClearClick: VoidFunction; onOnlyClick: (value: string | boolean) => void; @@ -835,7 +837,7 @@ export const FilterGroup = ({ name, options, optionsLoading, - selectedValues = { included: new Set(), excluded: new Set() }, + selectedValues: _selectedValues, onChange, onClearClick, onOnlyClick, @@ -860,6 +862,11 @@ export const FilterGroup = ({ const [showDistributions, setShowDistributions] = useState(false); const [isFetchingDistribution, setIsFetchingDistribution] = useState(false); + const selectedValues: SelectedValues = useMemo( + () => _selectedValues ?? { included: new Set(), excluded: new Set() }, + [_selectedValues], + ); + const hasRange = selectedValues.range != null; const toggleShowDistributions = useCallback(() => { diff --git a/packages/app/src/components/DBSearchPageFilters/NestedFilterGroup.tsx b/packages/app/src/components/DBSearchPageFilters/NestedFilterGroup.tsx index bbd440b0..83cbf225 100644 --- a/packages/app/src/components/DBSearchPageFilters/NestedFilterGroup.tsx +++ b/packages/app/src/components/DBSearchPageFilters/NestedFilterGroup.tsx @@ -41,7 +41,7 @@ const MAX_VIRTUAL_LIST_HEIGHT = 350; export const NestedFilterGroup = ({ name, childFilters, - selectedValues = {}, + selectedValues: _selectedValues, onChange, onClearClick, onOnlyClick, @@ -60,6 +60,11 @@ export const NestedFilterGroup = ({ chartConfig, isLive, }: NestedFilterGroupProps) => { + const selectedValues: FilterState = useMemo( + () => _selectedValues ?? {}, + [_selectedValues], + ); + const totalFiltersSize = useMemo( () => Object.values(selectedValues).reduce( diff --git a/packages/app/src/components/DBTableChart.tsx b/packages/app/src/components/DBTableChart.tsx index 91a2e332..286918ba 100644 --- a/packages/app/src/components/DBTableChart.tsx +++ b/packages/app/src/components/DBTableChart.tsx @@ -31,7 +31,7 @@ export default function DBTableChart({ queryKeyPrefix, onSortingChange, sort: controlledSort, - hiddenColumns = [], + hiddenColumns, title, toolbarPrefix, toolbarSuffix, @@ -135,7 +135,7 @@ export default function DBTableChart({ } return Object.keys(rows?.[0]) - .filter(key => !hiddenColumns.includes(key)) + .filter(key => !hiddenColumns?.includes(key)) .map(key => ({ // If it's an alias, wrap in quotes to support a variety of formats (ex "Time (ms)", "Req/s", etc) id: aliasMap.includes(key) ? `"${key}"` : key, diff --git a/packages/app/src/components/HyperJson.tsx b/packages/app/src/components/HyperJson.tsx index 0ced5666..160a65bf 100644 --- a/packages/app/src/components/HyperJson.tsx +++ b/packages/app/src/components/HyperJson.tsx @@ -155,7 +155,7 @@ const Line = React.memo( value, disableMenu, isInParsedJson = false, - parsedJsonRootPath = [], + parsedJsonRootPath, }: { keyName: string; keyPath: string[]; @@ -230,7 +230,7 @@ const Line = React.memo( // This is the start of a new parsed JSON context return keyPath; } - return parsedJsonRootPath; + return parsedJsonRootPath ?? []; }, [isStringValueValidJson, keyPath, parsedJsonRootPath]); // Hide LineMenu when selecting text in the value @@ -320,10 +320,10 @@ const Line = React.memo( const MAX_TREE_NODE_ITEMS = 50; function TreeNode({ data, - keyPath = [], + keyPath: _keyPath, disableMenu = false, isInParsedJson = false, - parsedJsonRootPath = [], + parsedJsonRootPath, }: { data: object; keyPath?: string[]; @@ -333,6 +333,8 @@ function TreeNode({ }) { const [isExpanded, setIsExpanded] = React.useState(false); + const keyPath = React.useMemo(() => _keyPath ?? [], [_keyPath]); + const originalLength = React.useMemo(() => Object.keys(data).length, [data]); const visibleLines = React.useMemo(() => { return isExpanded diff --git a/packages/app/src/components/MetricAttributeHelperPanel.tsx b/packages/app/src/components/MetricAttributeHelperPanel.tsx index 112338f3..07c9c0d8 100644 --- a/packages/app/src/components/MetricAttributeHelperPanel.tsx +++ b/packages/app/src/components/MetricAttributeHelperPanel.tsx @@ -42,7 +42,7 @@ interface MetricAttributeHelperPanelProps { language: 'sql' | 'lucene'; metricMetadata?: MetricMetadata | null; onAddToWhere: (clause: string) => void; - onAddToGroupBy: (clause: string) => void; + onAddToGroupBy?: (clause: string) => void; } const CATEGORY_LABELS: Record = { @@ -170,7 +170,7 @@ interface AttributeValueListProps { language: 'sql' | 'lucene'; onAddToWhere: (clause: string) => void; onBack: () => void; - onAddToGroupBy: (clause: string) => void; + onAddToGroupBy?: (clause: string) => void; } function AttributeValueList({ @@ -217,7 +217,7 @@ function AttributeValueList({ attribute.name, 'sql', ); - onAddToGroupBy(clause); + onAddToGroupBy?.(clause); }, [attribute, onAddToGroupBy]); return ( @@ -234,14 +234,16 @@ function AttributeValueList({ - + {onAddToGroupBy && ( + + )} prev, @@ -152,7 +152,7 @@ export default function SlowestEventsTile({ limit: { limit: 200 }, dateRange, filters: [ - ...extraFilters, + ...(extraFilters ?? []), { type: 'sql', condition: `${expressions.durationInMillis} > ${roundedP95}`, diff --git a/packages/app/src/components/Table.tsx b/packages/app/src/components/Table.tsx index f2422bdf..0d3f292b 100644 --- a/packages/app/src/components/Table.tsx +++ b/packages/app/src/components/Table.tsx @@ -25,7 +25,7 @@ type TableProps | string[]> = { // TODO: Retire this component in favor of Mantine export const Table = | string[]>({ - data = [], + data, columns, emptyMessage, hideHeader, @@ -35,13 +35,13 @@ export const Table = | string[]>({ tableMeta, }: TableProps) => { const table = useReactTable({ - data, + data: data ?? [], columns, meta: tableMeta, getCoreRowModel: getCoreRowModel(), }); - if (!data.length) { + if (!data?.length) { return
{emptyMessage}
; } diff --git a/packages/app/src/components/charts/ChartErrorState.tsx b/packages/app/src/components/charts/ChartErrorState.tsx index d233932f..89be323f 100644 --- a/packages/app/src/components/charts/ChartErrorState.tsx +++ b/packages/app/src/components/charts/ChartErrorState.tsx @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import cx from 'classnames'; import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse'; import { Button, Code, Group, Modal, Stack, Text } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; @@ -45,7 +46,14 @@ export default function ChartErrorState({ }, [error]); return ( -
+
Error loading chart, please check your query or try again later. diff --git a/packages/app/tests/e2e/components/ChartEditorComponent.ts b/packages/app/tests/e2e/components/ChartEditorComponent.ts index d7bd3423..45769813 100644 --- a/packages/app/tests/e2e/components/ChartEditorComponent.ts +++ b/packages/app/tests/e2e/components/ChartEditorComponent.ts @@ -18,6 +18,7 @@ export class ChartEditorComponent { private readonly chartTypeInput: Locator; private readonly sourceSelector: Locator; private readonly metricSelector: Locator; + private readonly aggFnSelect: Locator; private readonly addOrRemoveAlertButton: Locator; private readonly webhookSelector: Locator; private readonly runQueryButton: Locator; @@ -29,6 +30,7 @@ export class ChartEditorComponent { this.chartTypeInput = page.getByTestId('chart-type-input'); this.sourceSelector = page.getByTestId('source-selector'); this.metricSelector = page.getByTestId('metric-name-selector'); + this.aggFnSelect = page.getByTestId('agg-fn-select'); this.addOrRemoveAlertButton = page.getByTestId('alert-button'); this.webhookSelector = page.getByTestId('select-webhook'); this.addNewWebhookButton = page.getByTestId('add-new-webhook-button'); @@ -99,6 +101,33 @@ export class ChartEditorComponent { } } + /** + * Select an aggregation function from the dropdown + */ + async selectAggFn(label: string) { + await this.aggFnSelect.click(); + await this.page.getByRole('option', { name: label }).click(); + } + + /** + * Get the currently selected aggregation function value + */ + async getSelectedAggFn(): Promise { + return this.aggFnSelect.inputValue(); + } + + /** + * Check if an aggregation function option is available in the dropdown + */ + async isAggFnOptionAvailable(label: string): Promise { + await this.aggFnSelect.click(); + const option = this.page.getByRole('option', { name: label }); + const visible = await option.isVisible().catch(() => false); + // Close the dropdown + await this.page.keyboard.press('Escape'); + return visible; + } + async clickAddAlert() { await this.addOrRemoveAlertButton.click(); this.addNewWebhookButton.waitFor({ @@ -250,6 +279,10 @@ export class ChartEditorComponent { return this.metricSelector; } + get aggFn() { + return this.aggFnSelect; + } + get alertButton() { return this.addOrRemoveAlertButton; } diff --git a/packages/app/tests/e2e/features/correlated-metric-source.spec.ts b/packages/app/tests/e2e/features/correlated-metric-source.spec.ts new file mode 100644 index 00000000..96143c20 --- /dev/null +++ b/packages/app/tests/e2e/features/correlated-metric-source.spec.ts @@ -0,0 +1,35 @@ +import { SearchPage } from '../page-objects/SearchPage'; +import { expect, test } from '../utils/base-test'; + +test.describe('Correlated Metric Source', { tag: ['@full-stack'] }, () => { + let searchPage: SearchPage; + + test.beforeEach(async ({ page }) => { + searchPage = new SearchPage(page); + }); + + test('should show alert when no correlated metric source is configured', async ({ + page, + }) => { + // Navigate to search page + await searchPage.goto(); + + // Select the source without metricSourceId + await searchPage.selectSource('E2E K8s Logs No Metrics'); + + // Search for K8s events that have k8s.pod.uid resource attribute + await searchPage.performSearch('ResourceAttributes.k8s.pod.uid:*'); + + // Click on first row to open side panel + await searchPage.table.clickFirstRow(); + + // Click the Infrastructure tab + await searchPage.sidePanel.clickTab('infrastructure'); + + // Assert the "No correlated metric source" alert is visible + await expect(page.getByText('No correlated metric source')).toBeVisible(); + await expect( + page.getByText('does not have a correlated metric source'), + ).toBeVisible(); + }); +}); diff --git a/packages/app/tests/e2e/features/dashboard.spec.ts b/packages/app/tests/e2e/features/dashboard.spec.ts index 1510bec0..4f7264d9 100644 --- a/packages/app/tests/e2e/features/dashboard.spec.ts +++ b/packages/app/tests/e2e/features/dashboard.spec.ts @@ -793,6 +793,41 @@ test.describe('Dashboard', { tag: ['@dashboard'] }, () => { }); }); + test( + 'should deselect and hide the Custom aggregation function when switching to a metric source', + { tag: '@full-stack' }, + async () => { + await test.step('Navigate to dashboard and open new tile editor', async () => { + await dashboardPage.openNewTileEditor(); + }); + + await test.step('Select the "Custom" aggregation function', async () => { + await dashboardPage.chartEditor.selectAggFn('Custom'); + const selectedAggFn = + await dashboardPage.chartEditor.getSelectedAggFn(); + expect(selectedAggFn).toBe('Custom'); + }); + + await test.step('Switch the source to a metric source', async () => { + await dashboardPage.chartEditor.selectSource( + DEFAULT_METRICS_SOURCE_NAME, + ); + }); + + await test.step('Verify the aggregation function was automatically changed away from "Custom"', async () => { + const selectedAggFn = + await dashboardPage.chartEditor.getSelectedAggFn(); + expect(selectedAggFn).toBe('Count of Events'); + }); + + await test.step('Verify the "Custom" option is NOT available in the aggregation dropdown', async () => { + const isCustomAvailable = + await dashboardPage.chartEditor.isAggFnOptionAvailable('Custom'); + expect(isCustomAvailable).toBe(false); + }); + }, + ); + test( 'should clear saved query when WHERE input is cleared and saved', {}, diff --git a/packages/app/tests/e2e/fixtures/e2e-fixtures.json b/packages/app/tests/e2e/fixtures/e2e-fixtures.json index 7ee876e5..d4e3f31b 100644 --- a/packages/app/tests/e2e/fixtures/e2e-fixtures.json +++ b/packages/app/tests/e2e/fixtures/e2e-fixtures.json @@ -92,6 +92,23 @@ "traceIdExpression": "TraceId", "spanIdExpression": "SpanId", "implicitColumnExpression": "Body" + }, + { + "id": "E2E K8s Logs No Metrics", + "kind": "log", + "name": "E2E K8s Logs No Metrics", + "connection": "local", + "from": { "databaseName": "default", "tableName": "e2e_otel_logs" }, + "timestampValueExpression": "TimestampTime", + "defaultTableSelectExpression": "Timestamp, ServiceName, SeverityText, Body", + "serviceNameExpression": "ServiceName", + "severityTextExpression": "SeverityText", + "eventAttributesExpression": "LogAttributes", + "resourceAttributesExpression": "ResourceAttributes", + "traceIdExpression": "TraceId", + "spanIdExpression": "SpanId", + "implicitColumnExpression": "Body", + "displayedTimestampValueExpression": "Timestamp" } ] } diff --git a/packages/common-utils/CHANGELOG.md b/packages/common-utils/CHANGELOG.md index 3cf4e5d9..2c19be33 100644 --- a/packages/common-utils/CHANGELOG.md +++ b/packages/common-utils/CHANGELOG.md @@ -1,5 +1,23 @@ # @hyperdx/common-utils +## 0.17.0 + +### Minor Changes + +- a15122b3: feat: new team setting for number of filters to fetch +- 941d0450: feat: support sample-weighted aggregations for sampled trace data + +### Patch Changes + +- 518bda7d: feat: Add dashboard template gallery +- 4e54d850: fix: show Map sub-fields in facet panel for non-LowCardinality value types +- 53ba1e39: feat: Add favoriting for dashboards and saved searches +- b7581db8: feat: Add more chart display units +- 48a8d32b: fix: Fixed bug preventing clicking into rows with nullable date types (and other misc type) columns. +- a55b151e: fix: render clickhouse keywords properly in codemirror +- 308da30b: feat: Add $\_\_sourceTable macro +- e5c7fdf9: feat: Add saved searches listing page + ## 0.16.2 ### Patch Changes diff --git a/packages/common-utils/package.json b/packages/common-utils/package.json index 277a59ae..1aac3904 100644 --- a/packages/common-utils/package.json +++ b/packages/common-utils/package.json @@ -1,7 +1,7 @@ { "name": "@hyperdx/common-utils", "description": "Common utilities for HyperDX application", - "version": "0.16.2", + "version": "0.17.0", "license": "MIT", "private": true, "files": [ diff --git a/packages/otel-collector/CHANGELOG.md b/packages/otel-collector/CHANGELOG.md index d2936c42..e18a2746 100644 --- a/packages/otel-collector/CHANGELOG.md +++ b/packages/otel-collector/CHANGELOG.md @@ -1,5 +1,7 @@ # @hyperdx/otel-collector +## 2.23.0 + ## 2.22.1 ### Patch Changes diff --git a/packages/otel-collector/package.json b/packages/otel-collector/package.json index 2bcfc537..ff1254a5 100644 --- a/packages/otel-collector/package.json +++ b/packages/otel-collector/package.json @@ -1,7 +1,7 @@ { "name": "@hyperdx/otel-collector", "description": "HyperDX OpenTelemetry Collector configuration and Docker image", - "version": "2.22.1", + "version": "2.23.0", "license": "MIT", "private": true } diff --git a/yarn.lock b/yarn.lock index 52902d56..5c1050e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3878,6 +3878,17 @@ __metadata: languageName: node linkType: hard +"@eslint-community/eslint-utils@npm:^4.9.1": + version: 4.9.1 + resolution: "@eslint-community/eslint-utils@npm:4.9.1" + dependencies: + eslint-visitor-keys: "npm:^3.4.3" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 10c0/dc4ab5e3e364ef27e33666b11f4b86e1a6c1d7cbf16f0c6ff87b1619b3562335e9201a3d6ce806221887ff780ec9d828962a290bb910759fd40a674686503f02 + languageName: node + linkType: hard + "@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1": version: 4.12.2 resolution: "@eslint-community/regexpp@npm:4.12.2" @@ -3892,6 +3903,92 @@ __metadata: languageName: node linkType: hard +"@eslint-react/ast@npm:3.0.0": + version: 3.0.0 + resolution: "@eslint-react/ast@npm:3.0.0" + dependencies: + "@typescript-eslint/types": "npm:^8.57.0" + "@typescript-eslint/typescript-estree": "npm:^8.57.0" + "@typescript-eslint/utils": "npm:^8.57.0" + string-ts: "npm:^2.3.1" + peerDependencies: + eslint: ^10.0.0 + typescript: "*" + checksum: 10c0/d22565903f78061a5b2b6cc093209c50b941311d0fe760dc8eb74b9a7792968801f02eba3de7e3ff981e25fc36bdcbd3b5c1e340941b201a57bebf5683b5cdc9 + languageName: node + linkType: hard + +"@eslint-react/core@npm:3.0.0": + version: 3.0.0 + resolution: "@eslint-react/core@npm:3.0.0" + dependencies: + "@eslint-react/ast": "npm:3.0.0" + "@eslint-react/shared": "npm:3.0.0" + "@eslint-react/var": "npm:3.0.0" + "@typescript-eslint/scope-manager": "npm:^8.57.0" + "@typescript-eslint/types": "npm:^8.57.0" + "@typescript-eslint/utils": "npm:^8.57.0" + ts-pattern: "npm:^5.9.0" + peerDependencies: + eslint: ^10.0.0 + typescript: "*" + checksum: 10c0/bc38ef9191dd4b8da213b86b02bce995edbbd5cd805cb89f7285eec4822c436f6978937f77a65d0fe6db90ed7657044872f945969753b184f5bcb9e1cc34c87a + languageName: node + linkType: hard + +"@eslint-react/eslint-plugin@npm:^3.0.0": + version: 3.0.0 + resolution: "@eslint-react/eslint-plugin@npm:3.0.0" + dependencies: + "@eslint-react/shared": "npm:3.0.0" + "@typescript-eslint/scope-manager": "npm:^8.57.0" + "@typescript-eslint/type-utils": "npm:^8.57.0" + "@typescript-eslint/types": "npm:^8.57.0" + "@typescript-eslint/utils": "npm:^8.57.0" + eslint-plugin-react-dom: "npm:3.0.0" + eslint-plugin-react-naming-convention: "npm:3.0.0" + eslint-plugin-react-rsc: "npm:3.0.0" + eslint-plugin-react-web-api: "npm:3.0.0" + eslint-plugin-react-x: "npm:3.0.0" + ts-api-utils: "npm:^2.4.0" + peerDependencies: + eslint: ^10.0.0 + typescript: "*" + checksum: 10c0/d52bdbdf058c58149bfb104e182e05b4e88280edc440e70aede334205cad280e79829217d7fbfeda1162eb7020a5e7bb5339a9e0ac6e8c1143096d0261146ec3 + languageName: node + linkType: hard + +"@eslint-react/shared@npm:3.0.0": + version: 3.0.0 + resolution: "@eslint-react/shared@npm:3.0.0" + dependencies: + "@typescript-eslint/utils": "npm:^8.57.0" + ts-pattern: "npm:^5.9.0" + zod: "npm:^4.3.6" + peerDependencies: + eslint: ^10.0.0 + typescript: "*" + checksum: 10c0/04af79b8aca7063cd093b0447d3eaa5067d1614bde019fc5a16df308eff0460a574440223504b230c06617a1ceeaf149730ca1ba6b863dc173baf7bf0e1b1ceb + languageName: node + linkType: hard + +"@eslint-react/var@npm:3.0.0": + version: 3.0.0 + resolution: "@eslint-react/var@npm:3.0.0" + dependencies: + "@eslint-react/ast": "npm:3.0.0" + "@eslint-react/shared": "npm:3.0.0" + "@typescript-eslint/scope-manager": "npm:^8.57.0" + "@typescript-eslint/types": "npm:^8.57.0" + "@typescript-eslint/utils": "npm:^8.57.0" + ts-pattern: "npm:^5.9.0" + peerDependencies: + eslint: ^10.0.0 + typescript: "*" + checksum: 10c0/1dcd34e44ad14c5be3889551c8bbb8a0eadf9b05a3bee343e8640a9953b27a2eeec9c9fc4997e56a4fff0b6a512ddb663d8411cf751e988201488a4207f9573e + languageName: node + linkType: hard + "@eslint/compat@npm:^2.0.0": version: 2.0.0 resolution: "@eslint/compat@npm:2.0.0" @@ -4133,7 +4230,7 @@ __metadata: "@ai-sdk/anthropic": "npm:^3.0.58" "@ai-sdk/openai": "npm:^3.0.47" "@esm2cjs/p-queue": "npm:^7.3.0" - "@hyperdx/common-utils": "npm:^0.16.2" + "@hyperdx/common-utils": "npm:^0.17.0" "@hyperdx/node-opentelemetry": "npm:^0.9.0" "@hyperdx/passport-local-mongoose": "npm:^9.0.1" "@opentelemetry/api": "npm:^1.8.0" @@ -4213,10 +4310,11 @@ __metadata: "@codemirror/lint": "npm:^6.0.0" "@codemirror/state": "npm:^6.0.0" "@dagrejs/dagre": "npm:^1.1.5" + "@eslint-react/eslint-plugin": "npm:^3.0.0" "@eslint/compat": "npm:^2.0.0" "@hookform/resolvers": "npm:^3.9.0" "@hyperdx/browser": "npm:^0.22.0" - "@hyperdx/common-utils": "npm:^0.16.2" + "@hyperdx/common-utils": "npm:^0.17.0" "@hyperdx/node-opentelemetry": "npm:^0.9.0" "@jedmao/location": "npm:^3.0.0" "@mantine/core": "npm:^7.17.8" @@ -4266,7 +4364,6 @@ __metadata: dayjs: "npm:^1.11.19" eslint-config-next: "npm:^16.0.10" eslint-plugin-playwright: "npm:^2.4.0" - eslint-plugin-react: "npm:^7.37.0" eslint-plugin-react-hook-form: "npm:^0.3.1" eslint-plugin-react-hooks: "npm:^7.0.1" eslint-plugin-storybook: "npm:10.1.4" @@ -4342,7 +4439,7 @@ __metadata: languageName: node linkType: hard -"@hyperdx/common-utils@npm:^0.16.2, @hyperdx/common-utils@workspace:packages/common-utils": +"@hyperdx/common-utils@npm:^0.17.0, @hyperdx/common-utils@workspace:packages/common-utils": version: 0.0.0-use.local resolution: "@hyperdx/common-utils@workspace:packages/common-utils" dependencies: @@ -10230,6 +10327,19 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/project-service@npm:8.57.2": + version: 8.57.2 + resolution: "@typescript-eslint/project-service@npm:8.57.2" + dependencies: + "@typescript-eslint/tsconfig-utils": "npm:^8.57.2" + "@typescript-eslint/types": "npm:^8.57.2" + debug: "npm:^4.4.3" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/f84e3165b0a214318d4bc119018b87c044170d7638945e84bd4cee2d752b62c1797ce722ca1161cd06f48512d0115ef75500e6c8fc01005ad4bb39fb48dd77bf + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:8.48.1": version: 8.48.1 resolution: "@typescript-eslint/scope-manager@npm:8.48.1" @@ -10240,6 +10350,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.57.2, @typescript-eslint/scope-manager@npm:^8.57.0": + version: 8.57.2 + resolution: "@typescript-eslint/scope-manager@npm:8.57.2" + dependencies: + "@typescript-eslint/types": "npm:8.57.2" + "@typescript-eslint/visitor-keys": "npm:8.57.2" + checksum: 10c0/532b1a97a5c2fce51400fa1a94e09615b4df84ce1f2d107206a3f3935074cada396a3e30f155582a698981832868e1afea1641ff779ad9456fdc94169b7def64 + languageName: node + linkType: hard + "@typescript-eslint/tsconfig-utils@npm:8.48.1, @typescript-eslint/tsconfig-utils@npm:^8.48.1": version: 8.48.1 resolution: "@typescript-eslint/tsconfig-utils@npm:8.48.1" @@ -10249,6 +10369,15 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/tsconfig-utils@npm:8.57.2, @typescript-eslint/tsconfig-utils@npm:^8.57.2": + version: 8.57.2 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.57.2" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/199dad2d96efc88ce94f5f3e12e97205537bf7a7152e56ef1d84dfbe7bd1babebea9b9f396c01b6c447505a4eb02c1cbbd2c28828c587b51b41b15d017a11d2f + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:8.48.1": version: 8.48.1 resolution: "@typescript-eslint/type-utils@npm:8.48.1" @@ -10265,6 +10394,22 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/type-utils@npm:^8.57.0": + version: 8.57.2 + resolution: "@typescript-eslint/type-utils@npm:8.57.2" + dependencies: + "@typescript-eslint/types": "npm:8.57.2" + "@typescript-eslint/typescript-estree": "npm:8.57.2" + "@typescript-eslint/utils": "npm:8.57.2" + debug: "npm:^4.4.3" + ts-api-utils: "npm:^2.4.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/9c479cd0e809d26b7da7b31e830520bc016aaf528bc10a8b8279374808cb76a27f1b4adc77c84156417dc70f6a9e8604f47717b555a27293da2b9b5cfda70411 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:8.48.1, @typescript-eslint/types@npm:^8.48.1": version: 8.48.1 resolution: "@typescript-eslint/types@npm:8.48.1" @@ -10272,6 +10417,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.57.2, @typescript-eslint/types@npm:^8.57.0, @typescript-eslint/types@npm:^8.57.2": + version: 8.57.2 + resolution: "@typescript-eslint/types@npm:8.57.2" + checksum: 10c0/3cd87dd77d28b3ac2fed56a17909b0d11633628d4d733aa148dfd7af72e2cc3ec0e6114b72fac0ff538e8a47e907b4b10dab4095170ae1bd73719ef0b8eaf2e7 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.48.1": version: 8.48.1 resolution: "@typescript-eslint/typescript-estree@npm:8.48.1" @@ -10291,6 +10443,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.57.2, @typescript-eslint/typescript-estree@npm:^8.57.0": + version: 8.57.2 + resolution: "@typescript-eslint/typescript-estree@npm:8.57.2" + dependencies: + "@typescript-eslint/project-service": "npm:8.57.2" + "@typescript-eslint/tsconfig-utils": "npm:8.57.2" + "@typescript-eslint/types": "npm:8.57.2" + "@typescript-eslint/visitor-keys": "npm:8.57.2" + debug: "npm:^4.4.3" + minimatch: "npm:^10.2.2" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.4.0" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/2c5d143f0abbafd07a45f0b956aab5d6487b27f74fe93bee93e0a3f8edc8913f1522faf8d7d5215f3809a8d12f5729910ea522156552f2481b66e6d05ab311ae + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:8.48.1, @typescript-eslint/utils@npm:^8.8.1": version: 8.48.1 resolution: "@typescript-eslint/utils@npm:8.48.1" @@ -10306,6 +10477,21 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:8.57.2, @typescript-eslint/utils@npm:^8.57.0": + version: 8.57.2 + resolution: "@typescript-eslint/utils@npm:8.57.2" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.57.2" + "@typescript-eslint/types": "npm:8.57.2" + "@typescript-eslint/typescript-estree": "npm:8.57.2" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/5771f3d4206004cc817a6556a472926b4c1c885dc448049c10ffab1d5aac7bd59450a391fb57ce8ef31a8367e9c8ddb3bc9370c4e83fc8b61f50fd5189390e8f + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:8.48.1": version: 8.48.1 resolution: "@typescript-eslint/visitor-keys@npm:8.48.1" @@ -10316,6 +10502,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.57.2": + version: 8.57.2 + resolution: "@typescript-eslint/visitor-keys@npm:8.57.2" + dependencies: + "@typescript-eslint/types": "npm:8.57.2" + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10c0/8ceb8c228bf97b3e4b343bf6e42a91998d2522f459eb6b53c6bfad4898a9df74295660893dee6b698bdbbda537e968bfc13a3c56fc341089ebfba13db766a574 + languageName: node + linkType: hard + "@uiw/codemirror-extensions-basic-setup@npm:4.23.3": version: 4.23.3 resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.23.3" @@ -11825,6 +12021,13 @@ __metadata: languageName: node linkType: hard +"birecord@npm:^0.1.1": + version: 0.1.1 + resolution: "birecord@npm:0.1.1" + checksum: 10c0/7c7f8c38caebd98bcbfc8a209eaa3044384636ea162757de670c8633b7d1064b3b58e4e3d3a48fe10e279cd39290a76a31a18956a1c76b2b13522b6cf0293238 + languageName: node + linkType: hard + "bl@npm:^4.0.3, bl@npm:^4.1.0": version: 4.1.0 resolution: "bl@npm:4.1.0" @@ -12918,6 +13121,13 @@ __metadata: languageName: node linkType: hard +"compare-versions@npm:^6.1.1": + version: 6.1.1 + resolution: "compare-versions@npm:6.1.1" + checksum: 10c0/415205c7627f9e4f358f571266422980c9fe2d99086be0c9a48008ef7c771f32b0fbe8e97a441ffedc3910872f917a0675fe0fe3c3b6d331cda6d8690be06338 + languageName: node + linkType: hard + "component-emitter@npm:^1.3.0": version: 1.3.0 resolution: "component-emitter@npm:1.3.0" @@ -15116,6 +15326,26 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-react-dom@npm:3.0.0": + version: 3.0.0 + resolution: "eslint-plugin-react-dom@npm:3.0.0" + dependencies: + "@eslint-react/ast": "npm:3.0.0" + "@eslint-react/core": "npm:3.0.0" + "@eslint-react/shared": "npm:3.0.0" + "@eslint-react/var": "npm:3.0.0" + "@typescript-eslint/scope-manager": "npm:^8.57.0" + "@typescript-eslint/types": "npm:^8.57.0" + "@typescript-eslint/utils": "npm:^8.57.0" + compare-versions: "npm:^6.1.1" + ts-pattern: "npm:^5.9.0" + peerDependencies: + eslint: ^10.0.0 + typescript: "*" + checksum: 10c0/00b4ec055b7bc2cc12becbed6da633e2574b25d2ff5cfc7f4d74b37b96d95b04464403f7ce583b95fa530bb52af5b7d31c9ad8a35b402b24623c0c2a7490ad35 + languageName: node + linkType: hard + "eslint-plugin-react-hook-form@npm:^0.3.1": version: 0.3.1 resolution: "eslint-plugin-react-hook-form@npm:0.3.1" @@ -15140,6 +15370,90 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-react-naming-convention@npm:3.0.0": + version: 3.0.0 + resolution: "eslint-plugin-react-naming-convention@npm:3.0.0" + dependencies: + "@eslint-react/ast": "npm:3.0.0" + "@eslint-react/core": "npm:3.0.0" + "@eslint-react/shared": "npm:3.0.0" + "@eslint-react/var": "npm:3.0.0" + "@typescript-eslint/scope-manager": "npm:^8.57.0" + "@typescript-eslint/type-utils": "npm:^8.57.0" + "@typescript-eslint/types": "npm:^8.57.0" + "@typescript-eslint/utils": "npm:^8.57.0" + compare-versions: "npm:^6.1.1" + string-ts: "npm:^2.3.1" + ts-pattern: "npm:^5.9.0" + peerDependencies: + eslint: ^10.0.0 + typescript: "*" + checksum: 10c0/71abffdba83e5070b98c2f246577e968423a9445dde237222721ba789c69646ec8cd701d98172ef7d5f9ca98ff0383c769a1c646df81ea46f3659c0406e441c8 + languageName: node + linkType: hard + +"eslint-plugin-react-rsc@npm:3.0.0": + version: 3.0.0 + resolution: "eslint-plugin-react-rsc@npm:3.0.0" + dependencies: + "@eslint-react/ast": "npm:3.0.0" + "@eslint-react/shared": "npm:3.0.0" + "@eslint-react/var": "npm:3.0.0" + "@typescript-eslint/scope-manager": "npm:^8.57.0" + "@typescript-eslint/type-utils": "npm:^8.57.0" + "@typescript-eslint/types": "npm:^8.57.0" + "@typescript-eslint/utils": "npm:^8.57.0" + ts-pattern: "npm:^5.9.0" + peerDependencies: + eslint: ^10.0.0 + typescript: "*" + checksum: 10c0/d129083595c805aba35c7faa753b9c76ed507b388d522cf4e0873dfabd93f50f8c350f9910c49588302ee80924ea551ec0b5725d4aa6ceb6836f0c2eb2fcc757 + languageName: node + linkType: hard + +"eslint-plugin-react-web-api@npm:3.0.0": + version: 3.0.0 + resolution: "eslint-plugin-react-web-api@npm:3.0.0" + dependencies: + "@eslint-react/ast": "npm:3.0.0" + "@eslint-react/core": "npm:3.0.0" + "@eslint-react/shared": "npm:3.0.0" + "@eslint-react/var": "npm:3.0.0" + "@typescript-eslint/scope-manager": "npm:^8.57.0" + "@typescript-eslint/types": "npm:^8.57.0" + "@typescript-eslint/utils": "npm:^8.57.0" + birecord: "npm:^0.1.1" + ts-pattern: "npm:^5.9.0" + peerDependencies: + eslint: ^10.0.0 + typescript: "*" + checksum: 10c0/7cafcd210eec97f16e70ce641e8d8daf2313e643caa8121bdcee5df6b107f5764e2331d993b67e5fc896a893cbaa30db9751df94284fc92fb30205f0240ee556 + languageName: node + linkType: hard + +"eslint-plugin-react-x@npm:3.0.0": + version: 3.0.0 + resolution: "eslint-plugin-react-x@npm:3.0.0" + dependencies: + "@eslint-react/ast": "npm:3.0.0" + "@eslint-react/core": "npm:3.0.0" + "@eslint-react/shared": "npm:3.0.0" + "@eslint-react/var": "npm:3.0.0" + "@typescript-eslint/scope-manager": "npm:^8.57.0" + "@typescript-eslint/type-utils": "npm:^8.57.0" + "@typescript-eslint/types": "npm:^8.57.0" + "@typescript-eslint/utils": "npm:^8.57.0" + compare-versions: "npm:^6.1.1" + string-ts: "npm:^2.3.1" + ts-api-utils: "npm:^2.4.0" + ts-pattern: "npm:^5.9.0" + peerDependencies: + eslint: ^10.0.0 + typescript: "*" + checksum: 10c0/a58af67830dd54ddf1acab60d6a78da516fee6072591b50454dbef83a5132f3e7e57d54a191e8b3a412556191f3558bbe9ed5d0fd3f77323311cd8f6a99b5ce2 + languageName: node + linkType: hard + "eslint-plugin-react@npm:^7.37.0": version: 7.37.5 resolution: "eslint-plugin-react@npm:7.37.5" @@ -15232,6 +15546,13 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^5.0.0": + version: 5.0.1 + resolution: "eslint-visitor-keys@npm:5.0.1" + checksum: 10c0/16190bdf2cbae40a1109384c94450c526a79b0b9c3cb21e544256ed85ac48a4b84db66b74a6561d20fe6ab77447f150d711c2ad5ad74df4fcc133736bce99678 + languageName: node + linkType: hard + "eslint@npm:^9.39.1": version: 9.39.1 resolution: "eslint@npm:9.39.1" @@ -20803,6 +21124,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^10.2.2": + version: 10.2.4 + resolution: "minimatch@npm:10.2.4" + dependencies: + brace-expansion: "npm:^5.0.2" + checksum: 10c0/35f3dfb7b99b51efd46afd378486889f590e7efb10e0f6a10ba6800428cf65c9a8dedb74427d0570b318d749b543dc4e85f06d46d2858bc8cac7e1eb49a95945 + languageName: node + linkType: hard + "minimatch@npm:^8.0.2": version: 8.0.4 resolution: "minimatch@npm:8.0.4" @@ -25714,6 +26044,13 @@ __metadata: languageName: node linkType: hard +"string-ts@npm:^2.3.1": + version: 2.3.1 + resolution: "string-ts@npm:2.3.1" + checksum: 10c0/14b2829934713bf6cdf7ea54d948283af345e5c505bfb505aca949ab67235cbc99a3e335581f8a91f68935ac52c0d44b32112618658371e5593d5a75f26b3048 + languageName: node + linkType: hard + "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -26811,6 +27148,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^2.4.0": + version: 2.5.0 + resolution: "ts-api-utils@npm:2.5.0" + peerDependencies: + typescript: ">=4.8.4" + checksum: 10c0/767849383c114e7f1971fa976b20e73ac28fd0c70d8d65c0004790bf4d8f89888c7e4cf6d5949f9c1beae9bc3c64835bef77bbe27fddf45a3c7b60cebcf85c8c + languageName: node + linkType: hard + "ts-dedent@npm:^2.0.0": version: 2.2.0 resolution: "ts-dedent@npm:2.2.0" @@ -26903,6 +27249,13 @@ __metadata: languageName: node linkType: hard +"ts-pattern@npm:^5.9.0": + version: 5.9.0 + resolution: "ts-pattern@npm:5.9.0" + checksum: 10c0/7640db25c39d29b287471b2b82d4f7b4674a02098c6ba4d10fed180adfb07d0e0c71930d9e59dc0d90654145e02fd320af63cf0df3c41e100d4154658a612a0a + languageName: node + linkType: hard + "tsc-alias@npm:^1.8.8": version: 1.8.8 resolution: "tsc-alias@npm:1.8.8" @@ -28575,7 +28928,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:^4.1.11": +"zod@npm:^4.1.11, zod@npm:^4.3.6": version: 4.3.6 resolution: "zod@npm:4.3.6" checksum: 10c0/860d25a81ab41d33aa25f8d0d07b091a04acb426e605f396227a796e9e800c44723ed96d0f53a512b57be3d1520f45bf69c0cb3b378a232a00787a2609625307