Merge branch 'main' into teeohhem/expand-e2e-coverage

This commit is contained in:
Tom Alexander 2026-04-06 10:14:30 -04:00 committed by GitHub
commit fd4094f3d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 1133 additions and 261 deletions

View file

@ -1,5 +0,0 @@
---
"@hyperdx/app": patch
---
feat: Group Dashboards and Searches by Tag

View file

@ -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.

View file

@ -1,5 +0,0 @@
---
"@hyperdx/app": patch
---
fix: Properly enable line wrap behavior in JSON viewer by default

View file

@ -1,6 +0,0 @@
---
"@hyperdx/common-utils": patch
"@hyperdx/app": patch
---
feat: Add dashboard template gallery

View file

@ -1,5 +0,0 @@
---
"@hyperdx/app": patch
---
fix: differentiate map indexing vs array indexing

View file

@ -1,5 +0,0 @@
---
"@hyperdx/app": patch
---
perf: Defer expensive hooks in collapsed filter groups and virtualize nested filter lists

View file

@ -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

View file

@ -1,6 +0,0 @@
---
"@hyperdx/app": patch
"@hyperdx/common-utils": patch
---
fix: show Map sub-fields in facet panel for non-LowCardinality value types

View file

@ -1,7 +0,0 @@
---
"@hyperdx/common-utils": patch
"@hyperdx/api": patch
"@hyperdx/app": patch
---
feat: Add favoriting for dashboards and saved searches

View file

@ -1,7 +0,0 @@
---
"@hyperdx/common-utils": patch
"@hyperdx/api": patch
"@hyperdx/app": patch
---
feat: Add more chart display units

View file

@ -1,5 +0,0 @@
---
"@hyperdx/app": patch
---
fix: optimize order by should factor in wider cases, including the
default otel_traces

View file

@ -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.

View file

@ -1,7 +0,0 @@
---
"@hyperdx/common-utils": minor
"@hyperdx/api": minor
"@hyperdx/app": minor
---
feat: new team setting for number of filters to fetch

View file

@ -1,6 +0,0 @@
---
"@hyperdx/common-utils": patch
"@hyperdx/app": patch
---
fix: render clickhouse keywords properly in codemirror

View file

@ -1,5 +0,0 @@
---
"@hyperdx/app": patch
---
fix: move help menu from footer to main nav links

View file

@ -1,6 +0,0 @@
---
"@hyperdx/common-utils": patch
"@hyperdx/app": patch
---
feat: Add $\_\_sourceTable macro

View file

@ -1,5 +0,0 @@
---
"@hyperdx/app": patch
---
fix: Fix query error when searching nested JSON values

View file

@ -1,6 +0,0 @@
---
"@hyperdx/common-utils": patch
"@hyperdx/app": patch
---
feat: Add saved searches listing page

View file

@ -1,5 +0,0 @@
---
"@hyperdx/app": patch
---
fix: Add source schema preview to SQL Charts and Trace Panel

View file

@ -1,5 +0,0 @@
---
"@hyperdx/app": patch
---
fix: replace sidebar collapse icons to align with ClickHouse collapse patterns

View file

@ -1,5 +0,0 @@
---
"@hyperdx/app": patch
---
fix: Fix flaky E2E tests

View file

@ -1,5 +0,0 @@
---
"@hyperdx/app": minor
---
feat: Add input filter pills below search input to make filters usage more clear on seach page.

View file

@ -1,5 +0,0 @@
---
"@hyperdx/app": patch
---
feat: use 1 minute window for searches

View file

@ -1,7 +0,0 @@
---
'@hyperdx/common-utils': minor
'@hyperdx/api': minor
'@hyperdx/app': minor
---
feat: support sample-weighted aggregations for sampled trace data

4
.env
View file

@ -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

View file

@ -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

261
.github/workflows/pr-triage.yml vendored Normal file
View file

@ -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: 515 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 = [
'<!-- pr-triage -->',
`## ${info.emoji} ${info.headline}`,
'',
info.detail,
signalList,
'',
`**Review process**: ${info.process}`,
`**SLA**: ${info.sla}`,
'',
`<details><summary>Stats</summary>`,
'',
`- Files changed: ${files.length}`,
`- Lines changed: ${linesChanged}`,
`- Branch: \`${branchName}\``,
`- Author: ${author}`,
'',
'</details>',
'',
`> 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('<!-- pr-triage -->'));
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}`);
}
}

View file

@ -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:

View file

@ -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

View file

@ -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",

View file

@ -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 (

View file

@ -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],
},
})),
}));

View file

@ -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;

View file

@ -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

View file

@ -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,

View file

@ -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",

View file

@ -784,7 +784,12 @@ function ClickhousePage() {
</ChartBox>
</Grid.Col>
<Grid.Col span={12}>
<ChartBox style={{ height: 400 }}>
<ChartBox
style={{
height: 400,
overflow: 'hidden',
}}
>
<Text size="sm" mb="md">
Slowest Queries
</Text>

View file

@ -367,27 +367,28 @@ const Tile = forwardRef(
>
{(chart.config.displayType === DisplayType.Line ||
chart.config.displayType === DisplayType.StackedBar ||
chart.config.displayType === DisplayType.Number) && (
<Indicator
size={alert?.state === AlertState.OK ? 6 : 8}
zIndex={1}
color={alertIndicatorColor}
processing={alert?.state === AlertState.ALERT}
label={!alert && <span className="fs-8">+</span>}
mr={4}
>
<Tooltip label={alertTooltip} withArrow>
<ActionIcon
data-testid={`tile-alerts-button-${chart.id}`}
variant="subtle"
size="sm"
onClick={onEditClick}
>
<IconBell size={16} />
</ActionIcon>
</Tooltip>
</Indicator>
)}
chart.config.displayType === DisplayType.Number) &&
!isRawSqlSavedChartConfig(chart.config) && (
<Indicator
size={alert?.state === AlertState.OK ? 6 : 8}
zIndex={1}
color={alertIndicatorColor}
processing={alert?.state === AlertState.ALERT}
label={!alert && <span className="fs-8">+</span>}
mr={4}
>
<Tooltip label={alertTooltip} withArrow>
<ActionIcon
data-testid={`tile-alerts-button-${chart.id}`}
variant="subtle"
size="sm"
onClick={onEditClick}
>
<IconBell size={16} />
</ActionIcon>
</Tooltip>
</Indicator>
)}
<ActionIcon
data-testid={`tile-duplicate-button-${chart.id}`}
@ -469,7 +470,7 @@ const Tile = forwardRef(
alertIndicatorColor,
alertTooltip,
availableSections,
chart.config.displayType,
chart.config,
chart.id,
chart.containerId,
hovered,

View file

@ -2077,7 +2077,11 @@ function DBSearchPage() {
whiteSpace: 'pre-wrap',
}}
>
<SQLPreview data={queryError.query} formatData />
<SQLPreview
data={queryError.query}
formatData
enableLineWrapping
/>
</Code>
</Box>
)}

View file

@ -217,19 +217,14 @@ const LegendRenderer = memo<{
selectedSeries?: Set<string>;
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 (
<div className={styles.legend}>
{shownItems.map((entry, index) => {
const isSelected = selectedSeries.has(entry.value);
const isSelected = !!selectedSeries?.has(entry.value);
const isDisabled = hasSelection && !isSelected;
return (
<ExpandableLegendItem
@ -290,7 +285,7 @@ const LegendRenderer = memo<{
<Popover.Dropdown p="xs">
<div className={styles.legendTooltipContent}>
{restItems.map((entry, index) => {
const isSelected = selectedSeries.has(entry.value);
const isSelected = !!selectedSeries?.has(entry.value);
const isDisabled = hasSelection && !isSelected;
return (
<ExpandableLegendItem

View file

@ -233,8 +233,8 @@ function ServiceSelectControlled({
export function EndpointLatencyChart({
source,
dateRange,
appliedConfig = {},
extraFilters = [],
appliedConfig,
extraFilters,
}: {
source: TTraceSource;
dateRange: [Date, Date];
@ -284,9 +284,9 @@ export function EndpointLatencyChart({
config={{
source: source.id,
...pickSourceConfigFields(source),
where: appliedConfig.where || '',
where: appliedConfig?.where || '',
whereLanguage:
(appliedConfig.whereLanguage ?? getStoredLanguage()) || 'sql',
(appliedConfig?.whereLanguage ?? getStoredLanguage()) || 'sql',
select: [
// Separate the aggregations from the conversion to ms so that AggregatingMergeTree MVs can be used
{
@ -323,8 +323,11 @@ export function EndpointLatencyChart({
},
],
filters: [
...extraFilters,
...getScopedFilters({ appliedConfig, expressions }),
...(extraFilters ?? []),
...getScopedFilters({
appliedConfig: appliedConfig ?? {},
expressions,
}),
],
numberFormat: MS_NUMBER_FORMAT,
dateRange,
@ -337,9 +340,9 @@ export function EndpointLatencyChart({
config={{
source: source.id,
...pickSourceConfigFields(source),
where: appliedConfig.where || '',
where: appliedConfig?.where || '',
whereLanguage:
(appliedConfig.whereLanguage ?? getStoredLanguage()) || 'sql',
(appliedConfig?.whereLanguage ?? getStoredLanguage()) || 'sql',
select: [
{
alias: 'data_nanoseconds',
@ -353,8 +356,11 @@ export function EndpointLatencyChart({
},
],
filters: [
...extraFilters,
...getScopedFilters({ appliedConfig, expressions }),
...(extraFilters ?? []),
...getScopedFilters({
appliedConfig: appliedConfig ?? {},
expressions,
}),
],
dateRange,
}}

View file

@ -13,10 +13,12 @@ function AggFnSelect({
value,
defaultValue,
onChange,
hideCustom,
}: {
value: string;
defaultValue: string;
onChange: (value: OnChangeValue) => 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<UseControllerProps<any>, 'name'>) {
const {
field: { onChange: onAggFnChange, value: aggFnValue },
@ -96,6 +100,7 @@ export function AggFnSelectControlled({
value={value}
defaultValue={defaultValue}
onChange={onChange}
hideCustom={hideCustom}
/>
);
}

View file

@ -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,

View file

@ -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}
/>
</div>
{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}
/>
)}
</>

View file

@ -16,7 +16,7 @@ export type HighlightedAttribute = {
};
export function DBHighlightedAttributesList({
attributes = [],
attributes,
}: {
attributes: HighlightedAttribute[];
}) {

View file

@ -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 (
<Stack my="md" gap={40}>
{!metricSource && !isLoadingMetricSource && (
<>
<Alert color="yellow" title="No correlated metric source">
<Text size="sm">
{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 ? (
<Anchor component="button" onClick={openEditModal}>
Source Settings
</Anchor>
) : (
<Anchor component={Link} href="/team">
Team Settings
</Anchor>
)}
.
</Text>
</Alert>
{IS_LOCAL_MODE && (
<Modal
size="xl"
opened={editModalOpened}
onClose={closeEditModal}
title="Edit Source"
>
<TableSourceForm sourceId={source.id} />
</Modal>
)}
</>
)}
{podUid && (
<div>
{metricSource && (

View file

@ -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,

View file

@ -320,7 +320,7 @@ function HyperJsonMenu({ rowData }: { rowData: any }) {
export function DBRowJsonViewer({
data,
jsonColumns = [],
jsonColumns,
}: {
data: any;
jsonColumns?: string[];

View file

@ -107,7 +107,7 @@ const DBRowSidePanel = ({
isNestedPanel = false,
setSubDrawerOpen,
onClose,
breadcrumbPath = [],
breadcrumbPath,
onBreadcrumbClick,
}: DBRowSidePanelProps & {
setSubDrawerOpen: Dispatch<SetStateAction<boolean>>;
@ -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();

View file

@ -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 */}
<BreadcrumbNavigation
breadcrumbPath={breadcrumbPath}
breadcrumbPath={breadCrumbPathWithDefault}
onNavigateToLevel={onBreadcrumbClick}
/>
@ -277,7 +285,7 @@ export default function DBRowSidePanelHeader({
)}
<AISummarizeButton rowData={rowData} severityText={severityText} />
<Box mt="xs">
<DBHighlightedAttributesList attributes={attributes} />
<DBHighlightedAttributesList attributes={attributesWithDefault} />
</Box>
</>
);

View file

@ -330,15 +330,17 @@ const FilterRangeDisplay = ({
);
};
type SelectedValues = {
included: Set<string | boolean>;
excluded: Set<string | boolean>;
range?: { min: number; max: number };
};
export type FilterGroupProps = {
name: string;
options: { value: string | boolean; label: string }[];
optionsLoading?: boolean;
selectedValues?: {
included: Set<string | boolean>;
excluded: Set<string | boolean>;
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(() => {

View file

@ -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(

View file

@ -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,

View file

@ -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

View file

@ -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<AttributeCategory, string> = {
@ -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({
</Badge>
</Group>
</UnstyledButton>
<Button
variant="secondary"
size="xs"
leftSection={<IconPlus size={14} />}
onClick={handleAddToGroupBy}
>
Group By
</Button>
{onAddToGroupBy && (
<Button
variant="secondary"
size="xs"
leftSection={<IconPlus size={14} />}
onClick={handleAddToGroupBy}
>
Group By
</Button>
)}
</Group>
<TextInput

View file

@ -21,7 +21,7 @@ export default function SlowestEventsTile({
title,
queryKeyPrefix,
enabled = true,
extraFilters = [],
extraFilters,
}: {
source: TTraceSource;
dateRange: [Date, Date];
@ -54,7 +54,7 @@ export default function SlowestEventsTile({
},
],
dateRange,
filters: [...extraFilters],
filters: extraFilters,
},
{
placeholderData: (prev: any) => prev,
@ -152,7 +152,7 @@ export default function SlowestEventsTile({
limit: { limit: 200 },
dateRange,
filters: [
...extraFilters,
...(extraFilters ?? []),
{
type: 'sql',
condition: `${expressions.durationInMillis} > ${roundedP95}`,

View file

@ -25,7 +25,7 @@ type TableProps<T extends Record<string, unknown> | string[]> = {
// TODO: Retire this component in favor of Mantine
export const Table = <T extends Record<string, unknown> | string[]>({
data = [],
data,
columns,
emptyMessage,
hideHeader,
@ -35,13 +35,13 @@ export const Table = <T extends Record<string, unknown> | string[]>({
tableMeta,
}: TableProps<T>) => {
const table = useReactTable({
data,
data: data ?? [],
columns,
meta: tableMeta,
getCoreRowModel: getCoreRowModel(),
});
if (!data.length) {
if (!data?.length) {
return <div className={styles.emptyMessage}>{emptyMessage}</div>;
}

View file

@ -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 (
<div className="h-100 w-100 d-flex g-1 flex-column align-items-center justify-content-center text-muted overflow-auto">
<div
className={cx(
'h-100 w-100 d-flex g-1 flex-column align-items-center text-muted overflow-scroll',
{
'justify-content-center': variant === 'collapsible',
},
)}
>
<Text ta="center" size="sm" my="sm">
Error loading chart, please check your query or try again later.
</Text>

View file

@ -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<string | null> {
return this.aggFnSelect.inputValue();
}
/**
* Check if an aggregation function option is available in the dropdown
*/
async isAggFnOptionAvailable(label: string): Promise<boolean> {
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;
}

View file

@ -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();
});
});

View file

@ -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',
{},

View file

@ -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"
}
]
}

View file

@ -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

View file

@ -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": [

View file

@ -1,5 +1,7 @@
# @hyperdx/otel-collector
## 2.23.0
## 2.22.1
### Patch Changes

View file

@ -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
}

363
yarn.lock
View file

@ -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