mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
feat: Add dashboard template gallery (#2010)
## Summary This PR adds a gallery of importable dashboard templates to the dashboards page. The existing Dashboard import functionality is modified to support importing dashboard templates which are included in the app source code bundle. ### Screenshots or video https://github.com/user-attachments/assets/eae37214-f012-44dd-83ef-086749846260 ### How to test locally or on Vercel This can be tested as shown above in the preview environment. ### References - Linear Issue: Closes HDX-3661 Closes HDX-3814 - Related PRs:
This commit is contained in:
parent
0cc1295d36
commit
518bda7d20
20 changed files with 1906 additions and 19 deletions
6
.changeset/dull-grapes-whisper.md
Normal file
6
.changeset/dull-grapes-whisper.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@hyperdx/common-utils": patch
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
feat: Add dashboard template gallery
|
||||
|
|
@ -9,3 +9,4 @@ OTEL_SERVICE_NAME="hdx-oss-dev-app"
|
|||
PORT=${HYPERDX_APP_PORT}
|
||||
NODE_OPTIONS="--max-http-header-size=131072"
|
||||
NEXT_PUBLIC_HYPERDX_BASE_PATH=
|
||||
NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:${HDX_DEV_OTEL_HTTP_PORT:-4318}"
|
||||
3
packages/app/pages/dashboards/templates.tsx
Normal file
3
packages/app/pages/dashboards/templates.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import DashboardTemplatesPage from '@/components/Dashboards/DashboardTemplatesPage';
|
||||
|
||||
export default DashboardTemplatesPage;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -10,6 +10,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||
import { convertToDashboardDocument } from '@hyperdx/common-utils/dist/core/utils';
|
||||
import { isRawSqlSavedChartConfig } from '@hyperdx/common-utils/dist/guards';
|
||||
import {
|
||||
type DashboardTemplate,
|
||||
DashboardTemplateSchema,
|
||||
SavedChartConfig,
|
||||
} from '@hyperdx/common-utils/dist/types';
|
||||
|
|
@ -20,9 +21,10 @@ import {
|
|||
Collapse,
|
||||
Container,
|
||||
Group,
|
||||
Input,
|
||||
Loader,
|
||||
Stack,
|
||||
Table,
|
||||
TagsInput,
|
||||
Text,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
|
|
@ -36,22 +38,19 @@ import {
|
|||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
import { PageHeader } from './components/PageHeader';
|
||||
import SelectControlled from './components/SelectControlled';
|
||||
import { useBrandDisplayName } from './theme/ThemeProvider';
|
||||
import api from './api';
|
||||
import { useConnections } from './connection';
|
||||
import { useCreateDashboard, useUpdateDashboard } from './dashboard';
|
||||
import { getDashboardTemplate } from './dashboardTemplates';
|
||||
import { withAppNav } from './layout';
|
||||
import { useSources } from './source';
|
||||
|
||||
// The schema for the JSON data we expect to receive
|
||||
const InputSchema = DashboardTemplateSchema;
|
||||
type Input = z.infer<typeof InputSchema>;
|
||||
|
||||
function FileSelection({
|
||||
onComplete,
|
||||
}: {
|
||||
onComplete: (input: Input | null) => void;
|
||||
onComplete: (input: DashboardTemplate | null) => void;
|
||||
}) {
|
||||
// The schema for the form data we expect to receive
|
||||
const FormSchema = z.object({ file: z.instanceof(File).nullable() });
|
||||
|
|
@ -79,7 +78,7 @@ function FileSelection({
|
|||
try {
|
||||
const text = await file.text();
|
||||
const data = JSON.parse(text);
|
||||
const parsed = InputSchema.parse(data); // throws if invalid
|
||||
const parsed = DashboardTemplateSchema.parse(data); // throws if invalid
|
||||
onComplete(parsed);
|
||||
} catch (e: any) {
|
||||
onComplete(null);
|
||||
|
|
@ -188,6 +187,7 @@ function FileSelection({
|
|||
|
||||
const MappingForm = z.object({
|
||||
dashboardName: z.string().min(1),
|
||||
tags: z.array(z.string()),
|
||||
sourceMappings: z.array(z.string()),
|
||||
connectionMappings: z.array(z.string()),
|
||||
filterSourceMappings: z.array(z.string()).optional(),
|
||||
|
|
@ -195,10 +195,11 @@ const MappingForm = z.object({
|
|||
|
||||
type MappingFormValues = z.infer<typeof MappingForm>;
|
||||
|
||||
function Mapping({ input }: { input: Input }) {
|
||||
function Mapping({ input }: { input: DashboardTemplate }) {
|
||||
const router = useRouter();
|
||||
const { data: sources } = useSources();
|
||||
const { data: connections } = useConnections();
|
||||
const { data: existingTags } = api.useTags();
|
||||
const [dashboardId] = useQueryParam('dashboardId', StringParam);
|
||||
|
||||
const { handleSubmit, getFieldState, control, setValue } =
|
||||
|
|
@ -206,6 +207,7 @@ function Mapping({ input }: { input: Input }) {
|
|||
resolver: zodResolver(MappingForm),
|
||||
defaultValues: {
|
||||
dashboardName: input.name,
|
||||
tags: input.tags ?? [],
|
||||
sourceMappings: input.tiles.map(() => ''),
|
||||
connectionMappings: input.tiles.map(() => ''),
|
||||
},
|
||||
|
|
@ -381,6 +383,7 @@ function Mapping({ input }: { input: Input }) {
|
|||
tiles: zippedTiles,
|
||||
filters: zippedFilters,
|
||||
name: data.dashboardName,
|
||||
tags: data.tags,
|
||||
});
|
||||
let _dashboardId = dashboardId;
|
||||
if (_dashboardId) {
|
||||
|
|
@ -423,6 +426,18 @@ function Mapping({ input }: { input: Input }) {
|
|||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="tags"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TagsInput
|
||||
label="Tags"
|
||||
placeholder="Add tags"
|
||||
data={existingTags?.data ?? []}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
|
|
@ -496,7 +511,7 @@ function Mapping({ input }: { input: Input }) {
|
|||
{createDashboard.isError && (
|
||||
<Text c="red">{createDashboard.error.toString()}</Text>
|
||||
)}
|
||||
<Button type="submit" loading={createDashboard.isPending}>
|
||||
<Button type="submit" loading={createDashboard.isPending} mb="md">
|
||||
Finish Import
|
||||
</Button>
|
||||
</Stack>
|
||||
|
|
@ -506,7 +521,21 @@ function Mapping({ input }: { input: Input }) {
|
|||
|
||||
function DBDashboardImportPage() {
|
||||
const brandName = useBrandDisplayName();
|
||||
const [input, setInput] = useState<Input | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const templateName = router.query.template as string | undefined;
|
||||
const isTemplate = !!templateName;
|
||||
const isLoadingRoute = !router.isReady;
|
||||
|
||||
const templateInput = useMemo(
|
||||
() => (templateName ? getDashboardTemplate(templateName) : undefined),
|
||||
[templateName],
|
||||
);
|
||||
|
||||
const [fileInput, setFileInput] = useState<DashboardTemplate | null>(null);
|
||||
const input = templateInput ?? fileInput;
|
||||
const isTemplateNotFound = isTemplate && !isLoadingRoute && !templateInput;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -517,6 +546,16 @@ function DBDashboardImportPage() {
|
|||
<Anchor component={Link} href="/dashboards/list" fz="sm" c="dimmed">
|
||||
Dashboards
|
||||
</Anchor>
|
||||
{isTemplate && (
|
||||
<Anchor
|
||||
component={Link}
|
||||
href="/dashboards/templates"
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
>
|
||||
Templates
|
||||
</Anchor>
|
||||
)}
|
||||
<Text fz="sm" c="dimmed">
|
||||
Import
|
||||
</Text>
|
||||
|
|
@ -524,11 +563,26 @@ function DBDashboardImportPage() {
|
|||
<div>
|
||||
<Container>
|
||||
<Stack gap="lg" mt="xl">
|
||||
<FileSelection
|
||||
onComplete={i => {
|
||||
setInput(i);
|
||||
}}
|
||||
/>
|
||||
{isLoadingRoute ? (
|
||||
<Loader mx="auto" />
|
||||
) : isTemplateNotFound ? (
|
||||
<Stack align="center" gap="sm" py="xl">
|
||||
<Text ta="center">Oops! We couldn't find that template.</Text>
|
||||
<Text ta="center">
|
||||
Try{' '}
|
||||
<Anchor component={Link} href="/dashboards/templates">
|
||||
browsing available templates
|
||||
</Anchor>
|
||||
.
|
||||
</Text>
|
||||
</Stack>
|
||||
) : !isTemplate ? (
|
||||
<FileSelection
|
||||
onComplete={i => {
|
||||
setFileInput(i);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{input && <Mapping input={input} />}
|
||||
</Stack>
|
||||
</Container>
|
||||
|
|
|
|||
33
packages/app/src/__tests__/dashboardTemplates.test.ts
Normal file
33
packages/app/src/__tests__/dashboardTemplates.test.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { DashboardTemplateSchema } from '@hyperdx/common-utils/dist/types';
|
||||
|
||||
const TEMPLATES_DIR = path.resolve(__dirname, '../dashboardTemplates');
|
||||
|
||||
const jsonFiles = fs
|
||||
.readdirSync(TEMPLATES_DIR)
|
||||
.filter(f => f.endsWith('.json'));
|
||||
|
||||
describe('dashboard templates', () => {
|
||||
it('should have at least one template', () => {
|
||||
expect(jsonFiles.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it.each(jsonFiles)('%s should be a valid DashboardTemplate', file => {
|
||||
const raw = fs.readFileSync(path.join(TEMPLATES_DIR, file), 'utf-8');
|
||||
const json = JSON.parse(raw);
|
||||
const result = DashboardTemplateSchema.safeParse(json);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`${file} failed validation:\n${result.error.issues.map(i => ` - ${i.path.join('.')}: ${i.message}`).join('\n')}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(jsonFiles)('%s should have a description', file => {
|
||||
const raw = fs.readFileSync(path.join(TEMPLATES_DIR, file), 'utf-8');
|
||||
const json = JSON.parse(raw);
|
||||
expect(json.description).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -727,6 +727,23 @@ describe('validateChartForm', () => {
|
|||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not validate valueExpression for metric sources', () => {
|
||||
const setError = jest.fn();
|
||||
const errors = validateChartForm(
|
||||
makeForm({
|
||||
source: 'source-metric',
|
||||
series: [{ ...seriesItem, aggFn: 'sum', valueExpression: '' }],
|
||||
}),
|
||||
metricSource,
|
||||
setError,
|
||||
);
|
||||
expect(
|
||||
errors.filter(
|
||||
e => typeof e.path === 'string' && e.path.includes('valueExpression'),
|
||||
),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not validate valueExpression for Markdown displayType', () => {
|
||||
const setError = jest.fn();
|
||||
const errors = validateChartForm(
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ export const validateChartForm = (
|
|||
if (
|
||||
!isRawSqlChart &&
|
||||
Array.isArray(form.series) &&
|
||||
source?.kind !== SourceKind.Metric &&
|
||||
form.displayType !== DisplayType.Markdown &&
|
||||
form.displayType !== DisplayType.Search
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
import { useMemo } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Anchor,
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@mantine/core';
|
||||
import { IconUpload } from '@tabler/icons-react';
|
||||
|
||||
import { DASHBOARD_TEMPLATES } from '@/dashboardTemplates';
|
||||
import { useBrandDisplayName } from '@/theme/ThemeProvider';
|
||||
|
||||
import { withAppNav } from '../../layout';
|
||||
|
||||
export default function DashboardTemplatesPage() {
|
||||
const brandName = useBrandDisplayName();
|
||||
|
||||
const templatesByTag = useMemo(() => {
|
||||
const groups = new Map<string, typeof DASHBOARD_TEMPLATES>();
|
||||
for (const t of DASHBOARD_TEMPLATES) {
|
||||
const tags = t.tags.length > 0 ? t.tags : ['Other'];
|
||||
for (const tag of tags) {
|
||||
const group = groups.get(tag) ?? [];
|
||||
group.push(t);
|
||||
groups.set(tag, group);
|
||||
}
|
||||
}
|
||||
return Array.from(groups.entries())
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(
|
||||
([tag, items]) =>
|
||||
[
|
||||
tag,
|
||||
items.slice().sort((a, b) => a.name.localeCompare(b.name)),
|
||||
] as const,
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div data-testid="dashboard-templates-page">
|
||||
<Head>
|
||||
<title>Dashboard Templates - {brandName}</title>
|
||||
</Head>
|
||||
<Breadcrumbs my="lg" ms="xs" fz="sm">
|
||||
<Anchor component={Link} href="/dashboards/list" fz="sm" c="dimmed">
|
||||
Dashboards
|
||||
</Anchor>
|
||||
<Text fz="sm" c="dimmed">
|
||||
Templates
|
||||
</Text>
|
||||
</Breadcrumbs>
|
||||
<Container maw={1200} py="lg" px="lg">
|
||||
<Stack gap="xl">
|
||||
{templatesByTag.map(([tag, templates]) => (
|
||||
<div key={tag}>
|
||||
<Text fw={500} size="sm" c="dimmed" mb="sm">
|
||||
{tag}
|
||||
</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }}>
|
||||
{templates.map(t => (
|
||||
<Card key={t.id} withBorder padding="lg" radius="sm">
|
||||
<Stack justify="space-between" h="100%">
|
||||
<Stack>
|
||||
<Text
|
||||
fw={500}
|
||||
lineClamp={1}
|
||||
mb="xs"
|
||||
style={{ minWidth: 0 }}
|
||||
title={t.name}
|
||||
>
|
||||
{t.name}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t.description}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Button
|
||||
component={Link}
|
||||
href={`/dashboards/import?template=${t.id}`}
|
||||
variant="secondary"
|
||||
leftSection={<IconUpload size={16} />}
|
||||
mt="md"
|
||||
size="xs"
|
||||
data-testid={`import-template-${t.id}`}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DashboardTemplatesPage.getLayout = withAppNav;
|
||||
|
|
@ -5,6 +5,7 @@ import Router from 'next/router';
|
|||
import { useQueryState } from 'nuqs';
|
||||
import {
|
||||
ActionIcon,
|
||||
Anchor,
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
|
|
@ -173,11 +174,16 @@ export default function DashboardsListPage() {
|
|||
<Text fw={500} size="sm" c="dimmed" mb="sm">
|
||||
Preset Dashboards
|
||||
</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} mb="xl">
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} mb="sm">
|
||||
{PRESET_DASHBOARDS.map(p => (
|
||||
<ListingCard key={p.href} {...p} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<Text ta="right" mb="sm">
|
||||
<Anchor component={Link} href="/dashboards/templates" fz="sm">
|
||||
Browse dashboard templates →
|
||||
</Anchor>
|
||||
</Text>
|
||||
|
||||
{favoritedDashboards.length > 0 && (
|
||||
<>
|
||||
|
|
|
|||
357
packages/app/src/dashboardTemplates/dotnet-runtime.json
Normal file
357
packages/app/src/dashboardTemplates/dotnet-runtime.json
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
{
|
||||
"version": "0.1.0",
|
||||
"name": ".NET Runtime Metrics",
|
||||
"description": "Garbage collection, heap fragmentation, exception, thread pool, and CPU metrics for .NET v9+ applications with the OpenTelemetry.Instrumentation.Runtime package",
|
||||
"tags": [
|
||||
"OTel Runtime Metrics"
|
||||
],
|
||||
"tiles": [
|
||||
{
|
||||
"id": "6d4b7e",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "GC Heap Size",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "dotnet.gc.last_collection.heap.size",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "07f54d",
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "Exceptions",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "dotnet.exceptions",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "number",
|
||||
"mantissa": 0,
|
||||
"thousandSeparated": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c9bc67",
|
||||
"x": 12,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "GC Pause (s)",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "dotnet.gc.pause.time",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "number",
|
||||
"mantissa": 3,
|
||||
"thousandSeparated": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "334016",
|
||||
"x": 18,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "ThreadPool Items",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "dotnet.thread_pool.work_item.count",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "number",
|
||||
"mantissa": 0,
|
||||
"thousandSeparated": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "98e92c",
|
||||
"x": 0,
|
||||
"y": 3,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "GC Collections by Generation",
|
||||
"source": "Metrics",
|
||||
"displayType": "stacked_bar",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "dotnet.gc.collections",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"groupBy": "Attributes['gc.heap.generation']"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "f6c44a",
|
||||
"x": 12,
|
||||
"y": 3,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "GC Pause Time (s)",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "dotnet.gc.pause.time",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "number",
|
||||
"mantissa": 3,
|
||||
"thousandSeparated": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "e54085",
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Last Collection Heap Size (by gen)",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "dotnet.gc.last_collection.heap.size",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"groupBy": "Attributes['gc.heap.generation']",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "34979f",
|
||||
"x": 12,
|
||||
"y": 10,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Heap Fragmentation (by gen)",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "dotnet.gc.last_collection.heap.fragmentation.size",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"groupBy": "Attributes['gc.heap.generation']",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "6649b7",
|
||||
"x": 0,
|
||||
"y": 17,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Exception Rate",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "dotnet.exceptions",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "3a8add",
|
||||
"x": 12,
|
||||
"y": 17,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Allocation Rate",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "dotnet.gc.heap.total_allocated",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fdfdc8",
|
||||
"x": 0,
|
||||
"y": 24,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "CPU Cores Used",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "dotnet.process.cpu.time",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ebe6dc",
|
||||
"x": 12,
|
||||
"y": 24,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "ThreadPool Throughput & Lock Contention",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "dotnet.thread_pool.work_item.count",
|
||||
"metricType": "sum",
|
||||
"alias": "work items"
|
||||
},
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "dotnet.monitor.lock_contentions",
|
||||
"metricType": "sum",
|
||||
"alias": "contentions"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql"
|
||||
}
|
||||
}
|
||||
],
|
||||
"filters": [
|
||||
{
|
||||
"id": "svc-filter-001",
|
||||
"type": "QUERY_EXPRESSION",
|
||||
"name": "ServiceName",
|
||||
"expression": "ServiceName",
|
||||
"source": "Metrics",
|
||||
"sourceMetricType": "sum",
|
||||
"where": "ResourceAttributes['telemetry.sdk.language'] = 'dotnet'",
|
||||
"whereLanguage": "sql"
|
||||
}
|
||||
]
|
||||
}
|
||||
347
packages/app/src/dashboardTemplates/go-runtime.json
Normal file
347
packages/app/src/dashboardTemplates/go-runtime.json
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
{
|
||||
"version": "0.1.0",
|
||||
"name": "Go Runtime Metrics",
|
||||
"description": "Memory usage, allocations, GC targets, CPU utilization, and goroutine metrics for Go applications with the OTel Runtime (v0.62+) and Host Metrics instrumentations",
|
||||
"tags": [
|
||||
"OTel Runtime Metrics"
|
||||
],
|
||||
"tiles": [
|
||||
{
|
||||
"id": "109853",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "Memory Used",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "go.memory.used",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "e6a3df",
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "Alloc Rate",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "go.memory.allocated",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "40f62f",
|
||||
"x": 12,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "GC Heap Target",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "go.memory.gc.goal",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2029f0",
|
||||
"x": 18,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "CPU Utilization",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "process.cpu.utilization",
|
||||
"metricType": "gauge"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "percent",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bbde66",
|
||||
"x": 0,
|
||||
"y": 3,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Memory: Used vs Limit vs GC Target",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "go.memory.used",
|
||||
"metricType": "sum",
|
||||
"alias": "used"
|
||||
},
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "go.memory.limit",
|
||||
"metricType": "sum",
|
||||
"alias": "limit"
|
||||
},
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "go.memory.gc.goal",
|
||||
"metricType": "sum",
|
||||
"alias": "gc target"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "deb616",
|
||||
"x": 12,
|
||||
"y": 3,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Memory Allocated (rate)",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "go.memory.allocated",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "432607",
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "CPU Utilization",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "process.cpu.utilization",
|
||||
"metricType": "gauge"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "percent",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "08224d",
|
||||
"x": 12,
|
||||
"y": 10,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "CPU Cores Used",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "process.cpu.time",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fae8d7",
|
||||
"x": 0,
|
||||
"y": 17,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Object Allocations",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "go.memory.allocations",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "number",
|
||||
"mantissa": 0,
|
||||
"thousandSeparated": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "545a68",
|
||||
"x": 12,
|
||||
"y": 17,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Memory Used by Type",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "go.memory.used",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"groupBy": "Attributes['go.memory.type']",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c0fc04",
|
||||
"x": 0,
|
||||
"y": 24,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "GC Target (%) & GOMAXPROCS",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "go.config.gogc",
|
||||
"metricType": "sum",
|
||||
"alias": "GC target %"
|
||||
},
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "go.processor.limit",
|
||||
"metricType": "sum",
|
||||
"alias": "GOMAXPROCS"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql"
|
||||
}
|
||||
}
|
||||
],
|
||||
"filters": [
|
||||
{
|
||||
"id": "svc-filter-001",
|
||||
"type": "QUERY_EXPRESSION",
|
||||
"name": "ServiceName",
|
||||
"expression": "ServiceName",
|
||||
"source": "Metrics",
|
||||
"sourceMetricType": "sum",
|
||||
"where": "ResourceAttributes['telemetry.sdk.language'] = 'go'",
|
||||
"whereLanguage": "sql"
|
||||
}
|
||||
]
|
||||
}
|
||||
52
packages/app/src/dashboardTemplates/index.ts
Normal file
52
packages/app/src/dashboardTemplates/index.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import {
|
||||
type DashboardTemplate,
|
||||
DashboardTemplateSchema,
|
||||
} from '@hyperdx/common-utils/dist/types';
|
||||
|
||||
import dotnetRuntime from './dotnet-runtime.json';
|
||||
import goRuntime from './go-runtime.json';
|
||||
import jvmRuntimeMetrics from './jvm-runtime-metrics.json';
|
||||
import nodejsRuntime from './nodejs-runtime.json';
|
||||
|
||||
function parseTemplate(
|
||||
id: string,
|
||||
json: unknown,
|
||||
): DashboardTemplate | undefined {
|
||||
const result = DashboardTemplateSchema.safeParse(json);
|
||||
if (!result.success) {
|
||||
// This should not happen, we have a unit test to catch invalid templates.
|
||||
console.error(`Error parsing dashboard template "${id}":`, result.error);
|
||||
return undefined;
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
const templates: Record<string, unknown> = {
|
||||
'dotnet-runtime': dotnetRuntime,
|
||||
'go-runtime': goRuntime,
|
||||
'jvm-runtime-metrics': jvmRuntimeMetrics,
|
||||
'nodejs-runtime': nodejsRuntime,
|
||||
};
|
||||
|
||||
export const DASHBOARD_TEMPLATES = Object.entries(templates)
|
||||
.map(([id, template]) => {
|
||||
const parsedTemplate = parseTemplate(id, template);
|
||||
if (!parsedTemplate) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name: parsedTemplate.name,
|
||||
description: parsedTemplate.description ?? '',
|
||||
tags: parsedTemplate.tags ?? [],
|
||||
};
|
||||
})
|
||||
.filter(t => t !== undefined);
|
||||
|
||||
export function getDashboardTemplate(
|
||||
id: string,
|
||||
): DashboardTemplate | undefined {
|
||||
const json = templates[id];
|
||||
return parseTemplate(id, json);
|
||||
}
|
||||
389
packages/app/src/dashboardTemplates/jvm-runtime-metrics.json
Normal file
389
packages/app/src/dashboardTemplates/jvm-runtime-metrics.json
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
{
|
||||
"version": "0.1.0",
|
||||
"name": "JVM Runtime Metrics",
|
||||
"description": "Heap memory, CPU utilization, threads, and GC metrics for JVM applications with the OTel Java Agent v2+ wth JVM v17+",
|
||||
"tags": [
|
||||
"OTel Runtime Metrics"
|
||||
],
|
||||
"tiles": [
|
||||
{
|
||||
"id": "dd919b",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "Heap Used",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.memory.used",
|
||||
"metricType": "sum",
|
||||
"aggCondition": "Attributes['jvm.memory.type'] = 'heap'",
|
||||
"aggConditionLanguage": "sql"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c847ba",
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "CPU Utilization",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.cpu.recent_utilization",
|
||||
"metricType": "gauge"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "percent",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "777a97",
|
||||
"x": 12,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "Thread Count",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.thread.count",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "number",
|
||||
"mantissa": 0,
|
||||
"thousandSeparated": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "1b82ef",
|
||||
"x": 18,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "CPU Count",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.cpu.count",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "number",
|
||||
"mantissa": 0,
|
||||
"thousandSeparated": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "6363a5",
|
||||
"x": 0,
|
||||
"y": 3,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Heap: Used vs Committed vs Limit",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.memory.used",
|
||||
"metricType": "sum",
|
||||
"aggCondition": "Attributes['jvm.memory.type'] = 'heap'",
|
||||
"aggConditionLanguage": "sql",
|
||||
"alias": "used"
|
||||
},
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.memory.committed",
|
||||
"metricType": "sum",
|
||||
"aggCondition": "Attributes['jvm.memory.type'] = 'heap'",
|
||||
"aggConditionLanguage": "sql",
|
||||
"alias": "committed"
|
||||
},
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.memory.limit",
|
||||
"metricType": "sum",
|
||||
"aggCondition": "Attributes['jvm.memory.type'] = 'heap'",
|
||||
"aggConditionLanguage": "sql",
|
||||
"alias": "limit"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "5abddd",
|
||||
"x": 12,
|
||||
"y": 3,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Retained Heap After GC (by pool)",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.memory.used_after_last_gc",
|
||||
"metricType": "sum",
|
||||
"aggCondition": "Attributes['jvm.memory.type'] = 'heap'",
|
||||
"aggConditionLanguage": "sql"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"groupBy": "Attributes['jvm.memory.pool.name']",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c4abeb",
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Heap Memory Used by Pool",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.memory.used",
|
||||
"metricType": "sum",
|
||||
"aggCondition": "Attributes['jvm.memory.type'] = 'heap'",
|
||||
"aggConditionLanguage": "sql"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"groupBy": "Attributes['jvm.memory.pool.name']",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "26aa01",
|
||||
"x": 12,
|
||||
"y": 10,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Non-Heap Memory (metaspace)",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.memory.used",
|
||||
"metricType": "sum",
|
||||
"aggCondition": "Attributes['jvm.memory.type'] = 'non_heap'",
|
||||
"aggConditionLanguage": "sql"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"groupBy": "Attributes['jvm.memory.pool.name']",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ea6e2d",
|
||||
"x": 0,
|
||||
"y": 17,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "CPU Utilization",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.cpu.recent_utilization",
|
||||
"metricType": "gauge"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "percent",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bf01ef",
|
||||
"x": 12,
|
||||
"y": 17,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "CPU Cores Used",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.cpu.time",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "f90a9e",
|
||||
"x": 0,
|
||||
"y": 24,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Threads by State",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.thread.count",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"groupBy": "Attributes['jvm.thread.state']"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "17f41d",
|
||||
"x": 12,
|
||||
"y": 24,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Loaded Classes",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.class.count",
|
||||
"metricType": "sum",
|
||||
"alias": "current"
|
||||
},
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "jvm.class.loaded",
|
||||
"metricType": "sum",
|
||||
"alias": "loaded/s"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql"
|
||||
}
|
||||
}
|
||||
],
|
||||
"filters": [
|
||||
{
|
||||
"id": "svc-filter-001",
|
||||
"type": "QUERY_EXPRESSION",
|
||||
"name": "ServiceName",
|
||||
"expression": "ServiceName",
|
||||
"source": "Metrics",
|
||||
"sourceMetricType": "sum",
|
||||
"where": "ResourceAttributes['telemetry.sdk.language'] = 'java'",
|
||||
"whereLanguage": "sql"
|
||||
}
|
||||
]
|
||||
}
|
||||
324
packages/app/src/dashboardTemplates/nodejs-runtime.json
Normal file
324
packages/app/src/dashboardTemplates/nodejs-runtime.json
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
{
|
||||
"version": "0.1.0",
|
||||
"name": "Node.js Runtime Metrics",
|
||||
"description": "Event loop delay, heap usage, CPU utilization, and V8 memory for Node.js applications with OTel Runtime and Host Metrics instrumentations",
|
||||
"tags": [
|
||||
"OTel Runtime Metrics"
|
||||
],
|
||||
"tiles": [
|
||||
{
|
||||
"id": "55ef66",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "EL Delay p99 (ms)",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "nodejs.eventloop.delay.p99",
|
||||
"metricType": "gauge"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "number",
|
||||
"mantissa": 1,
|
||||
"thousandSeparated": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "31e132",
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "Event Loop Util",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "nodejs.eventloop.utilization",
|
||||
"metricType": "gauge"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "percent",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "52f4a5",
|
||||
"x": 12,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "Heap Used",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "v8js.memory.heap.used",
|
||||
"metricType": "gauge"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2029f0",
|
||||
"x": 18,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
"config": {
|
||||
"name": "CPU Utilization",
|
||||
"source": "Metrics",
|
||||
"displayType": "number",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "process.cpu.utilization",
|
||||
"metricType": "gauge"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "percent",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "f9dae7",
|
||||
"x": 0,
|
||||
"y": 3,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Event Loop Delay Percentiles (ms)",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "nodejs.eventloop.delay.p50",
|
||||
"metricType": "gauge",
|
||||
"alias": "p50"
|
||||
},
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "nodejs.eventloop.delay.p90",
|
||||
"metricType": "gauge",
|
||||
"alias": "p90"
|
||||
},
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "nodejs.eventloop.delay.p99",
|
||||
"metricType": "gauge",
|
||||
"alias": "p99"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "number",
|
||||
"mantissa": 1,
|
||||
"thousandSeparated": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "93c11e",
|
||||
"x": 12,
|
||||
"y": 3,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Event Loop Utilization",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "nodejs.eventloop.utilization",
|
||||
"metricType": "gauge"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "percent",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "7155d4",
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "V8 Heap: Used vs Limit",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "v8js.memory.heap.used",
|
||||
"metricType": "gauge",
|
||||
"alias": "used"
|
||||
},
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "v8js.memory.heap.limit",
|
||||
"metricType": "gauge",
|
||||
"alias": "limit"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c2ea50",
|
||||
"x": 12,
|
||||
"y": 10,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "V8 Heap Spaces (physical size)",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "v8js.memory.heap.space.physical_size",
|
||||
"metricType": "gauge"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"groupBy": "Attributes['v8js.heap.space.name']",
|
||||
"numberFormat": {
|
||||
"output": "byte",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ea6e2d",
|
||||
"x": 0,
|
||||
"y": 17,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "CPU Utilization",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "avg",
|
||||
"valueExpression": "",
|
||||
"metricName": "process.cpu.utilization",
|
||||
"metricType": "gauge"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql",
|
||||
"numberFormat": {
|
||||
"output": "percent",
|
||||
"mantissa": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "a73b57",
|
||||
"x": 12,
|
||||
"y": 17,
|
||||
"w": 12,
|
||||
"h": 7,
|
||||
"config": {
|
||||
"name": "Event Loop Busy Time",
|
||||
"source": "Metrics",
|
||||
"displayType": "line",
|
||||
"granularity": "auto",
|
||||
"alignDateRangeToGranularity": true,
|
||||
"select": [
|
||||
{
|
||||
"aggFn": "sum",
|
||||
"valueExpression": "",
|
||||
"metricName": "nodejs.eventloop.time",
|
||||
"metricType": "sum"
|
||||
}
|
||||
],
|
||||
"where": "",
|
||||
"whereLanguage": "sql"
|
||||
}
|
||||
}
|
||||
],
|
||||
"filters": [
|
||||
{
|
||||
"id": "svc-filter-001",
|
||||
"type": "QUERY_EXPRESSION",
|
||||
"name": "ServiceName",
|
||||
"expression": "ServiceName",
|
||||
"source": "Metrics",
|
||||
"sourceMetricType": "gauge",
|
||||
"where": "ResourceAttributes['telemetry.sdk.language'] = 'nodejs'",
|
||||
"whereLanguage": "sql"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import { DashboardImportPage } from '../page-objects/DashboardImportPage';
|
||||
import { DashboardPage } from '../page-objects/DashboardPage';
|
||||
import { DashboardsListPage } from '../page-objects/DashboardsListPage';
|
||||
import { expect, test } from '../utils/base-test';
|
||||
import { DEFAULT_METRICS_SOURCE_NAME } from '../utils/constants';
|
||||
|
||||
test.describe('Dashboard Template Import', { tag: ['@dashboard'] }, () => {
|
||||
let dashboardsListPage: DashboardsListPage;
|
||||
let dashboardImportPage: DashboardImportPage;
|
||||
let dashboardPage: DashboardPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
dashboardsListPage = new DashboardsListPage(page);
|
||||
dashboardImportPage = new DashboardImportPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
});
|
||||
|
||||
test(
|
||||
'should import a template from listing page through to new dashboard',
|
||||
{ tag: '@full-stack' },
|
||||
async ({ page }) => {
|
||||
await test.step('Navigate to dashboard listing page and verify templates link', async () => {
|
||||
await dashboardsListPage.goto();
|
||||
await expect(dashboardsListPage.pageContainer).toBeVisible();
|
||||
await expect(dashboardsListPage.browseTemplatesLink).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Navigate to templates page via Browse dashboard templates link', async () => {
|
||||
await dashboardsListPage.clickBrowseTemplates();
|
||||
await expect(page).toHaveURL(/\/dashboards\/templates/);
|
||||
await expect(dashboardImportPage.templatesPageContainer).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Verify template cards are listed', async () => {
|
||||
await expect(
|
||||
dashboardImportPage.getTemplateImportButton('dotnet-runtime'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
dashboardImportPage.getTemplateImportButton('jvm-runtime-metrics'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Click Import on the .NET Runtime Metrics template', async () => {
|
||||
await dashboardImportPage.clickImportTemplate('dotnet-runtime');
|
||||
await expect(page).toHaveURL(
|
||||
/\/dashboards\/import\?template=dotnet-runtime/,
|
||||
);
|
||||
});
|
||||
|
||||
await test.step('Verify the import mapping page loaded correctly', async () => {
|
||||
// File upload dropzone is not rendered in template mode
|
||||
await expect(dashboardImportPage.fileUploadDropzone).toBeHidden();
|
||||
// Step 2 mapping form is visible
|
||||
await expect(dashboardImportPage.mappingStepHeading).toBeVisible();
|
||||
// Dashboard name is pre-filled from the template
|
||||
await expect(dashboardImportPage.dashboardNameInput).toHaveValue(
|
||||
'.NET Runtime Metrics',
|
||||
);
|
||||
// A tile name from the .NET template is shown in the mapping table
|
||||
await expect(page.getByText('GC Heap Size')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Map the first source dropdown to E2E Metrics', async () => {
|
||||
await dashboardImportPage.selectSourceMapping(
|
||||
DEFAULT_METRICS_SOURCE_NAME,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
await test.step('Submit the import and verify success notification', async () => {
|
||||
await dashboardImportPage.finishImportButton.click();
|
||||
await expect(
|
||||
dashboardImportPage.getImportSuccessNotification(),
|
||||
).toBeVisible();
|
||||
await page.waitForURL(/\/dashboards\/.+/);
|
||||
});
|
||||
|
||||
await test.step('Verify the new dashboard has the correct name', async () => {
|
||||
await expect(page).toHaveURL(/\/dashboards\/.+/);
|
||||
await expect(
|
||||
dashboardPage.getDashboardHeading('.NET Runtime Metrics'),
|
||||
).toBeVisible();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'should show error for invalid template name',
|
||||
{ tag: '@full-stack' },
|
||||
async () => {
|
||||
await test.step('Navigate to import page with a nonexistent template param', async () => {
|
||||
await dashboardImportPage.gotoImport('nonexistent-template');
|
||||
});
|
||||
|
||||
await test.step('Verify template-not-found error and link to templates', async () => {
|
||||
await expect(dashboardImportPage.templateNotFoundText).toBeVisible();
|
||||
await expect(
|
||||
dashboardImportPage.browseAvailableTemplatesLink,
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
dashboardImportPage.browseAvailableTemplatesLink,
|
||||
).toHaveAttribute('href', '/dashboards/templates');
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
70
packages/app/tests/e2e/page-objects/DashboardImportPage.ts
Normal file
70
packages/app/tests/e2e/page-objects/DashboardImportPage.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* DashboardImportPage - Page object for dashboard template browsing and import
|
||||
* Covers the /dashboards/templates page and the /dashboards/import page
|
||||
*/
|
||||
import { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class DashboardImportPage {
|
||||
readonly page: Page;
|
||||
readonly templatesPageContainer: Locator;
|
||||
readonly mappingStepHeading: Locator;
|
||||
readonly dashboardNameInput: Locator;
|
||||
readonly finishImportButton: Locator;
|
||||
readonly fileUploadDropzone: Locator;
|
||||
readonly templateNotFoundText: Locator;
|
||||
readonly browseAvailableTemplatesLink: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.templatesPageContainer = page.getByTestId('dashboard-templates-page');
|
||||
this.mappingStepHeading = page.getByText('Step 2: Map Data');
|
||||
this.dashboardNameInput = page.getByLabel('Dashboard Name');
|
||||
this.finishImportButton = page.getByRole('button', {
|
||||
name: 'Finish Import',
|
||||
});
|
||||
this.fileUploadDropzone = page.getByText('Drag and drop a JSON file here', {
|
||||
exact: false,
|
||||
});
|
||||
this.templateNotFoundText = page.getByText(
|
||||
"Oops! We couldn't find that template.",
|
||||
);
|
||||
this.browseAvailableTemplatesLink = page.getByRole('link', {
|
||||
name: 'browsing available templates',
|
||||
});
|
||||
}
|
||||
|
||||
async gotoTemplates() {
|
||||
await this.page.goto('/dashboards/templates', { waitUntil: 'networkidle' });
|
||||
}
|
||||
|
||||
async gotoImport(templateId?: string) {
|
||||
const url = templateId
|
||||
? `/dashboards/import?template=${templateId}`
|
||||
: '/dashboards/import';
|
||||
await this.page.goto(url, { waitUntil: 'networkidle' });
|
||||
}
|
||||
|
||||
getTemplateImportButton(templateId: string) {
|
||||
return this.page.getByTestId(`import-template-${templateId}`);
|
||||
}
|
||||
|
||||
async clickImportTemplate(templateId: string) {
|
||||
await this.getTemplateImportButton(templateId).click();
|
||||
await this.page.waitForURL(`**/dashboards/import?template=${templateId}`);
|
||||
}
|
||||
|
||||
getSourceMappingSelect(index = 0) {
|
||||
return this.page.getByPlaceholder('Select a source').nth(index);
|
||||
}
|
||||
|
||||
async selectSourceMapping(sourceName: string, index = 0) {
|
||||
await this.getSourceMappingSelect(index).click();
|
||||
await this.page
|
||||
.getByRole('option', { name: sourceName, exact: true })
|
||||
.click();
|
||||
}
|
||||
|
||||
getImportSuccessNotification() {
|
||||
return this.page.getByText('Import Successful!');
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ export class DashboardsListPage {
|
|||
readonly tempDashboardButton: Locator;
|
||||
readonly gridViewButton: Locator;
|
||||
readonly listViewButton: Locator;
|
||||
readonly browseTemplatesLink: Locator;
|
||||
|
||||
private readonly emptyCreateDashboardButton: Locator;
|
||||
private readonly emptyImportDashboardButton: Locator;
|
||||
|
|
@ -29,6 +30,9 @@ export class DashboardsListPage {
|
|||
this.tempDashboardButton = page.getByTestId('temp-dashboard-button');
|
||||
this.gridViewButton = page.getByRole('button', { name: 'Grid view' });
|
||||
this.listViewButton = page.getByRole('button', { name: 'List view' });
|
||||
this.browseTemplatesLink = page.getByRole('link', {
|
||||
name: /Browse dashboard templates/,
|
||||
});
|
||||
this.emptyCreateDashboardButton = page.getByTestId(
|
||||
'empty-create-dashboard-button',
|
||||
);
|
||||
|
|
@ -42,6 +46,11 @@ export class DashboardsListPage {
|
|||
await this.page.goto('/dashboards/list', { waitUntil: 'networkidle' });
|
||||
}
|
||||
|
||||
async clickBrowseTemplates() {
|
||||
await this.browseTemplatesLink.click();
|
||||
await this.page.waitForURL('**/dashboards/templates');
|
||||
}
|
||||
|
||||
async searchDashboards(query: string) {
|
||||
await this.searchInput.fill(query);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -544,6 +544,7 @@ describe('utils', () => {
|
|||
expect(template).toEqual({
|
||||
name: 'My Dashboard',
|
||||
version: '0.1.0',
|
||||
tags: ['tag1', 'tag2'],
|
||||
tiles: [
|
||||
{
|
||||
id: 'tile1',
|
||||
|
|
@ -665,6 +666,7 @@ describe('utils', () => {
|
|||
expect(template).toEqual({
|
||||
name: 'My Dashboard',
|
||||
version: '0.1.0',
|
||||
tags: ['tag1', 'tag2'],
|
||||
tiles: [
|
||||
{
|
||||
id: 'tile1',
|
||||
|
|
|
|||
|
|
@ -466,6 +466,7 @@ export function convertToDashboardTemplate(
|
|||
const output: DashboardTemplate = {
|
||||
version: '0.1.0',
|
||||
name: input.name,
|
||||
tags: input.tags.length > 0 ? input.tags : undefined,
|
||||
tiles: [],
|
||||
};
|
||||
|
||||
|
|
@ -530,7 +531,7 @@ export function convertToDashboardDocument(
|
|||
const output: DashboardWithoutId = {
|
||||
name: input.name,
|
||||
tiles: [],
|
||||
tags: [],
|
||||
tags: input.tags ?? [],
|
||||
};
|
||||
|
||||
// expecting that input.tiles[0-n].config.source fields are already converted to ids
|
||||
|
|
|
|||
|
|
@ -764,9 +764,12 @@ export const DashboardTemplateSchema = DashboardWithoutIdSchema.omit({
|
|||
tags: true,
|
||||
}).extend({
|
||||
version: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
tiles: z.array(TileTemplateSchema),
|
||||
filters: z.array(DashboardFilterSchema).optional(),
|
||||
});
|
||||
export type DashboardTemplate = z.infer<typeof DashboardTemplateSchema>;
|
||||
|
||||
export const ConnectionSchema = z.object({
|
||||
id: z.string(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue