diff --git a/package.json b/package.json index aa2a2ee83..e0a80e447 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "release": "release-it", "release:ci": "release-it --ci", "release:test": "release-it --dry-run --ci --no-git.requireCleanWorkingDir", - "server": "docker-compose up --build", + "server": "docker-compose rm -f && docker-compose up --build", "server-dev": "GRAFANA_IMAGE='grafana-dev' GRAFANA_VERSION='10.2.0-131258-ubuntu' yarn server", "sign": "npx --yes @grafana/sign-plugin@latest", "spellcheck": "cspell -c cspell.config.json \"**/*.{ts,tsx,js,go,md,mdx,yml,yaml,json,scss,css}\"", @@ -111,6 +111,7 @@ "punycode": "^2.1.1", "react": "18.2.0", "react-async-hook": "^3.6.1", + "react-data-table-component": "^7.5.4", "react-dom": "18.2.0", "react-hook-form": "7.2.3", "react-popper": "^2.2.5", diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx new file mode 100644 index 000000000..9a2c92158 --- /dev/null +++ b/src/components/Table/Table.tsx @@ -0,0 +1,137 @@ +import React from 'react'; +import DataTable, { ExpanderComponentProps, TableColumn } from 'react-data-table-component'; +import { GrafanaConfig, GrafanaTheme2 } from '@grafana/data'; +import { Icon, Pagination, Tooltip, useStyles2 } from '@grafana/ui'; +import { css } from '@emotion/css'; + +import { createTableTheme } from './tableTheme'; + +interface Props { + // Data to be displayed in the table + data: T[]; + // Column definitions + columns: Array>; + noDataText: string; + // Actions to be performed when a row is clicked + onRowClicked?(row: T): void; + // Show pagination component, uses pagination from Grafana UI + pagination: boolean; + paginationPerPage?: number; + pointerOnHover?: boolean; + id: string; + name: string; + className?: string; + dataTableProps?: any; + defaultSortField?: string; + expandableRows?: boolean; + // Component to be displayed when a row is expanded + expandableComponent?: React.FC> | null; + // Requires config object from '@grafana/runtime' + config: GrafanaConfig; + expandTooltipText?: string; +} + +export const Table = ({ + data, + className, + columns, + noDataText, + onRowClicked, + pagination, + paginationPerPage = 15, + pointerOnHover = true, + id, + name, + dataTableProps = {}, + defaultSortField = undefined, + expandableRows = false, + expandableComponent = null, + config, + expandTooltipText = 'Actions and additional data', +}: Props) => { + const styles = useStyles2(getStyles); + // Uses light/dark theme based on user config in Grafana + createTableTheme(config.theme2.isDark); + const expandColor = config.theme2.isDark ? 'white' : 'black'; + + return ( + (onRowClicked ? onRowClicked(row) : undefined)} + paginationPerPage={paginationPerPage} + expandableRows={expandableRows} + expandableRowsComponent={expandableComponent} + expandableIcon={{ + collapsed: ( + + + + ), + expanded: , + }} + paginationComponent={({ currentPage, rowCount, rowsPerPage, onChangePage }) => ( +
+ { + onChangePage(toPage, rowCount); + }} + /> +
+ )} + {...dataTableProps} + /> + ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + paginationWrapper: css` + display: flex; + margin: ${theme.spacing(2)} 0 ${theme.spacing(1)}; + align-items: flex-end; + justify-content: flex-end; + position: relative; + `, + pageSize: css` + margin-right: ${theme.spacing(1)}; + `, + expandRow: css` + padding: 20px 20px 20px 65px; + display: flex; + flex-direction: row; + justify-content: space-between; + background: ${theme.colors.background.secondary}; + `, + expandRowData: css` + display: flex; + flex-direction: row; + justify-content: flex-start; + + div { + margin-right: 50px; + + p:first-child { + font-weight: bold; + } + } + `, + expandRowActions: css` + display: flex; + flex-direction: row; + justify-content: flex-end; + `, +}); + +export type { TableColumn, ExpanderComponentProps }; diff --git a/src/components/Table/index.ts b/src/components/Table/index.ts new file mode 100644 index 000000000..75193adc3 --- /dev/null +++ b/src/components/Table/index.ts @@ -0,0 +1 @@ +export * from './Table'; diff --git a/src/components/Table/tableTheme.ts b/src/components/Table/tableTheme.ts new file mode 100644 index 000000000..199a6f6a1 --- /dev/null +++ b/src/components/Table/tableTheme.ts @@ -0,0 +1,52 @@ +import { createTheme } from 'react-data-table-component'; + +export const createTableTheme = (isDark: boolean) => { + if (isDark) { + createTheme('grafana-dark', { + text: { + primary: 'rgb(204, 204, 220)', + secondary: '#2aa198', + }, + background: { + default: '#181b1f;', + }, + context: { + background: '#cb4b16', + text: '#FFFFFF', + }, + divider: { + default: 'rgba(204, 204, 220, 0.07)', + }, + highlightOnHover: { + default: '#111217', + text: '#FFFFFF', + }, + sortFocus: { + default: '#2aa198', + }, + }); + } else { + createTheme('grafana-light', { + text: { + primary: 'rgb(36, 41, 46);', + secondary: '#2aa198', + }, + background: { + default: 'rgb(255, 255, 255);', + }, + context: { + background: '#cb4b16', + text: '#FFFFFF', + }, + divider: { + default: 'rgba(36, 41, 46, 0.12)', + }, + highlightOnHover: { + default: 'rgb(247, 247, 247)', + }, + sortFocus: { + default: '#2aa198', + }, + }); + } +}; diff --git a/src/scenes/MULTIHTTP/assertionTable.ts b/src/scenes/MULTIHTTP/assertionTable.ts index 047163aee..903eb61cd 100644 --- a/src/scenes/MULTIHTTP/assertionTable.ts +++ b/src/scenes/MULTIHTTP/assertionTable.ts @@ -9,7 +9,27 @@ function getQueryRunner(logs: DataSourceRef) { queries: [ { editorMode: 'code', - expr: 'sum (\n min_over_time (\n {job="$job", instance="$instance"}\n | logfmt method, url, check, value, msg\n | __error__ = ""\n | msg = "check result"\n | unwrap value\n [$__range]\n )\n) by (method, url, check)\n/\ncount (\n min_over_time (\n {job="$job", instance="$instance"}\n | logfmt method, url, check, value, msg\n | __error__ = ""\n | msg = "check result"\n | unwrap value\n [$__range]\n )\n) by (method, url, check)', + expr: `sum ( + min_over_time ( + {job="$job", instance="$instance"} + | logfmt method, url, check, value, msg + | __error__ = "" + | msg = "check result" + | unwrap value + [$__range] + ) + ) by (method, url, check) + / + count ( + min_over_time ( + {job="$job", instance="$instance"} + | logfmt method, url, check, value, msg + | __error__ = "" + | msg = "check result" + | unwrap value + [$__range] + ) + ) by (method, url, check)`, queryType: 'instant', refId: 'A', }, diff --git a/src/scenes/SCRIPTED/AssertionsTable/AssertionsTable.tsx b/src/scenes/SCRIPTED/AssertionsTable/AssertionsTable.tsx new file mode 100644 index 000000000..3ac347f74 --- /dev/null +++ b/src/scenes/SCRIPTED/AssertionsTable/AssertionsTable.tsx @@ -0,0 +1,216 @@ +import React, { useMemo } from 'react'; +import { config } from '@grafana/runtime'; +import { + SceneComponentProps, + SceneDataTransformer, + SceneFlexItem, + sceneGraph, + SceneObject, + SceneObjectBase, + SceneObjectState, + SceneQueryRunner, +} from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; +import { LinkButton, useStyles2 } from '@grafana/ui'; + +import { Table, TableColumn } from 'components/Table'; + +import { getTablePanelStyles } from '../getTablePanelStyles'; +import { AssertionTableRow } from './AssertionsTableRow'; + +function getQueryRunner(logs: DataSourceRef) { + const query = new SceneQueryRunner({ + datasource: logs, + queries: [ + { + refId: 'A', + expr: `count_over_time ( + {job="$job", instance="$instance"} + | logfmt check, value, msg + | __error__ = "" + | msg = "check result" + | value = "1" + | keep check + [$__range] + ) + / + count_over_time ( + {job="$job", instance="$instance"} + | logfmt check, msg + | __error__ = "" + | msg = "check result" + | keep check + [$__range] + ) + `, + queryType: 'instant', + }, + { + refId: 'B', + expr: `count_over_time ( + {job="$job", instance="$instance"} + | logfmt check, value, msg + | __error__ = "" + | msg = "check result" + | value = "1" + | keep check + [$__range] + ) + `, + queryType: 'instant', + }, + { + refId: 'C', + expr: `count_over_time ( + {job="$job", instance="$instance"} + | logfmt check, value, msg + | __error__ = "" + | msg = "check result" + | value = "0" + | keep check + [$__range] + ) + `, + queryType: 'instant', + }, + ], + }); + + return new SceneDataTransformer({ + $data: query, + transformations: [ + { + id: 'joinByField', + options: { + byField: 'check', + mode: 'outer', + }, + }, + { + id: 'organize', + options: { + excludeByName: { + 'Time 1': true, + 'Time 2': true, + 'Time 3': true, + }, + indexByName: {}, + renameByName: { + 'Value #A': 'Success rate', + 'Value #B': 'Success count', + 'Value #C': 'Failure count', + }, + }, + }, + ], + }); +} + +export interface DataRow { + name: string; + successRate: number; + logs: DataSourceRef; + successCount: number; + failureCount: number; +} + +function AssertionsTable({ model }: SceneComponentProps) { + const { data } = sceneGraph.getData(model).useState(); + const { logs } = model.useState(); + const styles = useStyles2(getTablePanelStyles); + + const columns = useMemo>>(() => { + return [ + { name: 'Assertion', selector: (row) => row.name }, + { + name: 'Success', + selector: (row) => { + let successRate; + if (isNaN(row.successRate)) { + successRate = 'N/A'; + } else { + successRate = row.successRate.toFixed(2) + '%'; + } + return successRate; + }, + }, + { name: 'Success count', selector: (row) => row.successCount }, + { name: 'Failure count', selector: (row) => row.failureCount }, + ]; + }, []); + + const tableData = useMemo(() => { + if (!data) { + return []; + } + const fields = data.series[0]?.fields; + return ( + fields?.[0].values.reduce((acc, name, index) => { + const successRate = fields[1].values[index] * 100; + const successCount = fields[2].values[index]; + const failureCount = fields[3].values[index]; + acc.push({ name, successRate, logs, successCount, failureCount }); + return acc; + }, []) ?? [] + ); + }, [data, logs]); + + return ( +
+
+
+ Assertions +
+
+ + columns={columns} + data={tableData} + expandableRows + dataTableProps={{ + expandableRowsComponentProps: { tableViz: model, logs }, + }} + expandableComponent={AssertionTableRow} + //@ts-ignore - noDataText expects a string, but we want to render a component and it works + noDataText={ +
+

There are no assertions in this script. You can use k6 Checks to validate conditions in your script.

+ + Learn more about Checks + +
+ } + pagination={false} + id="assertion-table" + name="Assertions" + config={config} + /> +
+ ); +} + +interface AssertionsTableState extends SceneObjectState { + logs: DataSourceRef; + expandedRows?: SceneObject[]; +} + +export class AssertionsTableSceneObject extends SceneObjectBase { + static Component = AssertionsTable; + public constructor(state: AssertionsTableState) { + super(state); + } +} + +export function getAssertionTable(logs: DataSourceRef) { + return new SceneFlexItem({ + body: new AssertionsTableSceneObject({ + $data: getQueryRunner(logs), + logs, + expandedRows: [], + }), + }); +} diff --git a/src/scenes/SCRIPTED/AssertionsTable/AssertionsTableRow.tsx b/src/scenes/SCRIPTED/AssertionsTable/AssertionsTableRow.tsx new file mode 100644 index 000000000..45dfd6c4a --- /dev/null +++ b/src/scenes/SCRIPTED/AssertionsTable/AssertionsTableRow.tsx @@ -0,0 +1,44 @@ +import React, { useEffect } from 'react'; +import { ExpanderComponentProps } from 'react-data-table-component'; +import { SceneObjectBase, SceneObjectState } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { AssertionsTableSceneObject, DataRow } from './AssertionsTable'; +import { getSuccessOverTimeByProbe } from './successOverTimeByProbe'; + +export class AssertionsTableRowSceneObject extends SceneObjectBase { + static Component = AssertionTableRow; + + public constructor(state: AssertionsTableRowState) { + super(state); + } +} + +interface AssertionsTableRowState extends SceneObjectState { + logs: DataSourceRef; + job: string; + instance: string; + name: string; +} + +interface Props extends ExpanderComponentProps { + tableViz?: AssertionsTableSceneObject; + logs?: DataSourceRef; +} + +export function AssertionTableRow({ data, tableViz, logs }: Props) { + const { expandedRows } = tableViz?.useState() ?? {}; + const [rowKey, setRowKey] = React.useState(undefined); + const rowScene = expandedRows?.find((scene) => scene.state.key === rowKey); + + useEffect(() => { + if (!rowScene && logs && tableViz) { + // const newRowScene = getErrorLogs(logs, data.name); + const newRowScene = getSuccessOverTimeByProbe(logs, data.name); + setRowKey(newRowScene.state.key); + tableViz.setState({ expandedRows: [...(tableViz.state.expandedRows ?? []), newRowScene] }); + } + }, [data.name, tableViz, rowScene, logs]); + + return
{rowScene ? : null}
; +} diff --git a/src/scenes/SCRIPTED/AssertionsTable/errorLogs.ts b/src/scenes/SCRIPTED/AssertionsTable/errorLogs.ts new file mode 100644 index 000000000..813ee43a8 --- /dev/null +++ b/src/scenes/SCRIPTED/AssertionsTable/errorLogs.ts @@ -0,0 +1,52 @@ +import { SceneFlexItem, SceneFlexLayout, SceneQueryRunner } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { ExplorablePanel } from 'scenes/ExplorablePanel'; + +function getQueryRunner(logs: DataSourceRef, name: string) { + return new SceneQueryRunner({ + datasource: logs, + queries: [ + { + expr: ` + {job="$job", instance="$instance"} | + logfmt msg, level, method, url, check | + __error__ = "" | + msg = "check result" | + check="${name}" | + level != "debug" | + line_format "{{.method}} {{.url}} ➜ {{ if eq .value \\"1\\" }}PASS{{else}}FAIL{{end}}: {{.check}}" | + label_format level="{{ if eq .value \\"1\\" }}info{{else}}error{{end}}"`, + refId: 'A', + }, + ], + }); +} + +// scene: new SceneFlexItem({ +export function getErrorLogs(logs: DataSourceRef, name: string) { + const flexItem = new SceneFlexLayout({ + width: '100%', + height: 400, + children: [ + new SceneFlexItem({ + body: new ExplorablePanel({ + $data: getQueryRunner(logs, name), + options: { + showTime: true, + showLabels: false, + showCommonLabels: false, + wrapLogMessage: true, + prettifyLogMessage: false, + enableLogDetails: true, + dedupStrategy: 'none', + sortOrder: 'Descending', + }, + title: 'Error logs for ' + name, + pluginId: 'logs', + }), + }), + ], + }); + return flexItem; +} diff --git a/src/scenes/SCRIPTED/AssertionsTable/index.ts b/src/scenes/SCRIPTED/AssertionsTable/index.ts new file mode 100644 index 000000000..430162950 --- /dev/null +++ b/src/scenes/SCRIPTED/AssertionsTable/index.ts @@ -0,0 +1 @@ +export { getAssertionTable } from './AssertionsTable'; diff --git a/src/scenes/SCRIPTED/AssertionsTable/successOverTimeByProbe.ts b/src/scenes/SCRIPTED/AssertionsTable/successOverTimeByProbe.ts new file mode 100644 index 000000000..8698db7cb --- /dev/null +++ b/src/scenes/SCRIPTED/AssertionsTable/successOverTimeByProbe.ts @@ -0,0 +1,64 @@ +import { SceneFlexItem, SceneFlexLayout, SceneQueryRunner } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { ExplorablePanel } from 'scenes/ExplorablePanel'; + +function getQueryRunner(metrics: DataSourceRef, name: string) { + return new SceneQueryRunner({ + datasource: metrics, + queries: [ + { + expr: ` + count_over_time ( + {job="$job", instance="$instance"} + | logfmt check, value, msg, probe + | __error__ = "" + | msg = "check result" + | value = "1" + | check = "${name}" + | keep probe + [5m] + ) + / + count_over_time ( + {job="$job", instance="$instance"} + | logfmt check, msg, probe + | __error__ = "" + | msg = "check result" + | check = "${name}" + | keep probe + [5m] + ) + `, + refId: 'A', + queryType: 'range', + hide: false, + legendFormat: '{{ probe }}', + }, + ], + }); +} + +export function getSuccessOverTimeByProbe(metrics: DataSourceRef, name: string) { + return new SceneFlexLayout({ + height: 400, + children: [ + new SceneFlexItem({ + body: new ExplorablePanel({ + $data: getQueryRunner(metrics, name), + options: { + range: true, + }, + fieldConfig: { + defaults: { + unit: 'percentunit', + }, + overrides: [], + }, + title: 'Success rate by probe for ' + name, + pluginId: 'timeseries', + }), + }), + ], + }); +} diff --git a/src/scenes/SCRIPTED/ResultsByTargetTable/ResultByTargetTable.tsx b/src/scenes/SCRIPTED/ResultsByTargetTable/ResultByTargetTable.tsx new file mode 100644 index 000000000..31502d55c --- /dev/null +++ b/src/scenes/SCRIPTED/ResultsByTargetTable/ResultByTargetTable.tsx @@ -0,0 +1,178 @@ +import React, { useMemo } from 'react'; +import { TableColumn } from 'react-data-table-component'; +import { config } from '@grafana/runtime'; +import { + SceneComponentProps, + SceneFlexItem, + sceneGraph, + SceneObject, + SceneObjectBase, + SceneObjectState, + SceneQueryRunner, +} from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; +import { useStyles2 } from '@grafana/ui'; + +import { Table } from 'components/Table'; + +import { getTablePanelStyles } from '../getTablePanelStyles'; +import { ResultsByTargetTableRow } from './ResultsByTargetTableRow'; + +interface ResultsByTargetTableState extends SceneObjectState { + metrics: DataSourceRef; + expandedRows?: SceneObject[]; +} + +export interface DataRow { + name: string; + expectedResponse: number; + successRate: number; + latency: number; + metrics: DataSourceRef; +} + +export class ResultsByTargetTableSceneObject extends SceneObjectBase { + static Component = ({ model }: SceneComponentProps) => { + const { data } = sceneGraph.getData(model).useState(); + const { metrics } = model.useState(); + const styles = useStyles2(getTablePanelStyles); + + const columns = useMemo>>(() => { + return [ + { name: 'URL', selector: (row) => row.name }, + { + name: 'Success', + selector: (row) => { + let successRate; + if (isNaN(row.successRate)) { + successRate = 'N/A'; + } else { + successRate = row.successRate.toFixed(2) + '%'; + } + return successRate; + }, + }, + { + name: 'Expected response', + selector: (row) => { + let expectedResponse; + if (isNaN(row.expectedResponse)) { + expectedResponse = 'N/A'; + } else { + expectedResponse = row.expectedResponse.toFixed(2) + '%'; + } + return expectedResponse; + }, + }, + { + name: 'Latency', + selector: (row) => { + return (row.latency * 1000).toFixed(2) + 'ms'; + }, + }, + ]; + }, []); + + const tableData = useMemo(() => { + if (!data) { + return []; + } + const fields = data.series[0]?.fields; + return ( + fields?.[1].values.reduce((acc, name, index) => { + const successRate = fields[2].values[index] * 100; + const expectedResponse = data.series[1].fields[2].values[index] * 100; + const latency = data.series[2].fields[2].values[index] * 100; + + acc.push({ name, successRate, latency, expectedResponse, metrics }); + return acc; + }, []) ?? [] + ); + }, [data, metrics]); + + return ( +
+
+
+ Results by URL +
+
+ + columns={columns} + data={tableData} + expandableRows + dataTableProps={{ + expandableRowsComponentProps: { tableViz: model, metrics }, + }} + expandableComponent={ResultsByTargetTableRow} + noDataText={'No requests found'} + pagination={false} + id="assertion-table" + name="Assertions" + config={config} + /> +
+ ); + }; + + public constructor(state: ResultsByTargetTableState) { + super(state); + } +} + +function getQueryRunner(metrics: DataSourceRef) { + return new SceneQueryRunner({ + queries: [ + { + datasource: metrics, + editorMode: 'code', + exemplar: false, + expr: ` + sum by (name) (probe_http_requests_total{job="$job", instance="$instance"}) + / + count by (name) (probe_http_requests_total{job="$job", instance="$instance"})`, + format: 'table', + instant: true, + legendFormat: '__auto', + range: false, + refId: 'A', + }, + { + datasource: metrics, + editorMode: 'code', + exemplar: false, + expr: ` + sum by (name) (probe_http_got_expected_response{job="$job", instance="$instance"}) + / + count by (name)(probe_http_got_expected_response{job="$job", instance="$instance"})`, + format: 'table', + instant: true, + legendFormat: '__auto', + range: false, + refId: 'B', + }, + { + datasource: metrics, + editorMode: 'code', + exemplar: false, + // TODO: Does this make sense at all? I want get the total latency for each URL and then average the different probes, not just sum all the probes together + expr: `avg by (name) (sum by(name, probe)(rate(probe_http_duration_seconds{job="$job", instance="$instance"}[5m])))`, + format: 'table', + hide: false, + instant: true, + range: false, + refId: 'C', + }, + ], + }); +} + +export function getResultsByTargetTable(metrics: DataSourceRef) { + return new SceneFlexItem({ + body: new ResultsByTargetTableSceneObject({ + $data: getQueryRunner(metrics), + metrics, + expandedRows: [], + }), + }); +} diff --git a/src/scenes/SCRIPTED/ResultsByTargetTable/ResultsByTargetTableRow.tsx b/src/scenes/SCRIPTED/ResultsByTargetTable/ResultsByTargetTableRow.tsx new file mode 100644 index 000000000..92e52f099 --- /dev/null +++ b/src/scenes/SCRIPTED/ResultsByTargetTable/ResultsByTargetTableRow.tsx @@ -0,0 +1,50 @@ +import React, { useEffect } from 'react'; +import { ExpanderComponentProps } from 'react-data-table-component'; +import { SceneFlexLayout } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { getExpectedResponse } from '../expectedResponse'; +import { getDurationByTargetProbe } from './durationByTargetProbe'; +import { getLatencyByPhaseTarget } from './latencyByPhaseTarget'; +import { DataRow, ResultsByTargetTableSceneObject } from './ResultByTargetTable'; +import { getSuccessRateByTargetProbe } from './successRateByTargetProbe'; + +function getResultsByTargetRowScene(metrics: DataSourceRef, name: string) { + const flexItem = new SceneFlexLayout({ + direction: 'column', + children: [ + new SceneFlexLayout({ + width: '100%', + height: 250, + children: [getSuccessRateByTargetProbe(metrics, name), getExpectedResponse(metrics, name)], + }), + new SceneFlexLayout({ + width: '100%', + height: 250, + children: [getDurationByTargetProbe(metrics, name), getLatencyByPhaseTarget(metrics, name)], + }), + ], + }); + return flexItem; +} + +interface Props extends ExpanderComponentProps { + tableViz?: ResultsByTargetTableSceneObject; + metrics?: DataSourceRef; +} + +export function ResultsByTargetTableRow({ data, tableViz, metrics }: Props) { + const { expandedRows } = tableViz?.useState() ?? {}; + const [rowKey, setRowKey] = React.useState(undefined); + const rowScene = expandedRows?.find((scene) => scene.state.key === rowKey); + + useEffect(() => { + if (!rowScene && metrics && tableViz) { + const newRowScene = getResultsByTargetRowScene(metrics, data.name); + setRowKey(newRowScene.state.key); + tableViz.setState({ expandedRows: [...(tableViz.state.expandedRows ?? []), newRowScene] }); + } + }, [data.name, tableViz, rowScene, metrics]); + + return
{rowScene ? : null}
; +} diff --git a/src/scenes/SCRIPTED/ResultsByTargetTable/durationByTargetProbe.ts b/src/scenes/SCRIPTED/ResultsByTargetTable/durationByTargetProbe.ts new file mode 100644 index 000000000..04a5d1cbd --- /dev/null +++ b/src/scenes/SCRIPTED/ResultsByTargetTable/durationByTargetProbe.ts @@ -0,0 +1,36 @@ +import { SceneFlexItem, SceneQueryRunner } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { ExplorablePanel } from 'scenes/ExplorablePanel'; + +function getQueryRunner(metrics: DataSourceRef, name: string) { + return new SceneQueryRunner({ + datasource: metrics, + queries: [ + { + expr: `sum by (probe) (probe_http_total_duration_seconds{probe=~".*", job="$job", instance="$instance", name="${name}"})`, + refId: 'A', + legendFormat: '{{probe}}', + }, + ], + }); +} + +export function getDurationByTargetProbe(metrics: DataSourceRef, name: string) { + return new SceneFlexItem({ + body: new ExplorablePanel({ + $data: getQueryRunner(metrics, name), + options: { + instant: false, + }, + fieldConfig: { + defaults: { + unit: 's', + }, + overrides: [], + }, + title: 'Duration by probe for ' + name, + pluginId: 'timeseries', + }), + }); +} diff --git a/src/scenes/SCRIPTED/ResultsByTargetTable/errorLogs.ts b/src/scenes/SCRIPTED/ResultsByTargetTable/errorLogs.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/scenes/SCRIPTED/ResultsByTargetTable/latencyByPhaseTarget.ts b/src/scenes/SCRIPTED/ResultsByTargetTable/latencyByPhaseTarget.ts new file mode 100644 index 000000000..ef5cbd416 --- /dev/null +++ b/src/scenes/SCRIPTED/ResultsByTargetTable/latencyByPhaseTarget.ts @@ -0,0 +1,42 @@ +import { SceneFlexItem, SceneQueryRunner } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { ExplorablePanel } from 'scenes/ExplorablePanel'; + +function getQueryRunner(metrics: DataSourceRef, name: string) { + return new SceneQueryRunner({ + datasource: metrics, + queries: [ + { + refId: 'A', + expr: ` + sum by (phase) (probe_http_duration_seconds{job="$job", instance="$instance", name="${name}", probe=~"$probe"}) + / + count by (phase) (probe_http_duration_seconds{job="$job", instance="$instance", name="${name}", probe=~"$probe"}) + `, + legendFormat: '__auto', + range: true, + }, + ], + }); +} + +export function getLatencyByPhaseTarget(metrics: DataSourceRef, name: string) { + return new SceneFlexItem({ + body: new ExplorablePanel({ + $data: getQueryRunner(metrics, name), + pluginId: 'barchart', + title: `Latency by phase for ${name}`, + fieldConfig: { + defaults: { + unit: 's', + }, + overrides: [], + }, + options: { + stacking: 'normal', + xTickLabelSpacing: 75, + }, + }), + }); +} diff --git a/src/scenes/SCRIPTED/ResultsByTargetTable/successRateByTargetProbe.ts b/src/scenes/SCRIPTED/ResultsByTargetTable/successRateByTargetProbe.ts new file mode 100644 index 000000000..2e503bdf2 --- /dev/null +++ b/src/scenes/SCRIPTED/ResultsByTargetTable/successRateByTargetProbe.ts @@ -0,0 +1,45 @@ +import { SceneFlexItem, SceneQueryRunner } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { ExplorablePanel } from 'scenes/ExplorablePanel'; + +function getQueryRunner(metrics: DataSourceRef, name: string) { + const queries = [ + { + exemplar: true, + expr: `sum by (probe) ( + probe_http_requests_total{instance="$instance", job="$job", probe=~".*", name="${name}"} + ) + / + sum by (probe) ( + probe_http_requests_total{instance="$instance", job="$job", probe=~".*", name="${name}"} + )`, + hide: false, + range: true, + interval: '', + legendFormat: '{{probe}}', + refId: 'A', + }, + ]; + return new SceneQueryRunner({ + datasource: metrics, + queries, + }); +} + +export function getSuccessRateByTargetProbe(metrics: DataSourceRef, name: string) { + return new SceneFlexItem({ + body: new ExplorablePanel({ + pluginId: 'timeseries', + title: `Success rate by probe for ${name}`, + $data: getQueryRunner(metrics, name), + fieldConfig: { + overrides: [], + defaults: { + unit: 'percentunit', + max: 1, + }, + }, + }), + }); +} diff --git a/src/scenes/SCRIPTED/expectedResponse.ts b/src/scenes/SCRIPTED/expectedResponse.ts index dce80850a..640f9710d 100644 --- a/src/scenes/SCRIPTED/expectedResponse.ts +++ b/src/scenes/SCRIPTED/expectedResponse.ts @@ -3,61 +3,35 @@ import { DataSourceRef } from '@grafana/schema'; import { ExplorablePanel } from 'scenes/ExplorablePanel'; -function getQueryRunner(metrics: DataSourceRef) { +function getQueryRunner(metrics: DataSourceRef, name: string) { return new SceneQueryRunner({ datasource: metrics, queries: [ { expr: ` - sum by (name) (probe_http_got_expected_response{job="$job", instance="$instance"}) + sum by (probe) (probe_http_got_expected_response{job="$job", instance="$instance", name="${name}", probe=~"$probe"}) / - count by (name) (probe_http_got_expected_response{job="$job", instance="$instance"})`, - format: 'table', - instant: true, - legendFormat: '{{ name }}', + count by (probe) (probe_http_got_expected_response{job="$job", instance="$instance", name="${name}", probe=~"$probe"})`, + legendFormat: '{{ probe }}', range: false, - refId: 'B', + refId: 'A', }, ], }); } -export function getExpectedResponse(metrics: DataSourceRef) { +export function getExpectedResponse(metrics: DataSourceRef, name: string) { return new SceneFlexItem({ body: new ExplorablePanel({ - $data: getQueryRunner(metrics), - pluginId: 'table', - title: 'Expected response by target', + $data: getQueryRunner(metrics, name), + pluginId: 'timeseries', + title: 'Expected response by probe for ' + name, fieldConfig: { defaults: { unit: 'percentunit', + max: 1, }, - overrides: [ - { - matcher: { - id: 'byName', - options: 'Time', - }, - properties: [ - { - id: 'custom.hidden', - value: true, - }, - ], - }, - { - matcher: { - id: 'byName', - options: 'Value', - }, - properties: [ - { - id: 'displayName', - value: 'Expected status received', - }, - ], - }, - ], + overrides: [], }, }), }); diff --git a/src/scenes/SCRIPTED/getTablePanelStyles.ts b/src/scenes/SCRIPTED/getTablePanelStyles.ts new file mode 100644 index 000000000..e08ece82d --- /dev/null +++ b/src/scenes/SCRIPTED/getTablePanelStyles.ts @@ -0,0 +1,34 @@ +import { GrafanaTheme2 } from '@grafana/data'; +import { css } from '@emotion/css'; + +export function getTablePanelStyles(theme: GrafanaTheme2) { + return { + container: css({ + border: `1px solid ${theme.components.panel.borderColor}`, + width: '100%', + borderRadius: theme.shape.radius.default, + }), + title: css({ + label: 'panel-title', + display: 'flex', + marginBottom: 0, // override default h6 margin-bottom + padding: theme.spacing(theme.components.panel.padding), + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + fontSize: theme.typography.h6.fontSize, + fontWeight: theme.typography.h6.fontWeight, + }), + headerContainer: css({ + label: 'panel-header', + display: 'flex', + alignItems: 'center', + }), + noDataContainer: css({ + padding: theme.spacing(4), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }), + }; +} diff --git a/src/scenes/SCRIPTED/scriptedScene.ts b/src/scenes/SCRIPTED/scriptedScene.ts index 19296e2af..17f11ff56 100644 --- a/src/scenes/SCRIPTED/scriptedScene.ts +++ b/src/scenes/SCRIPTED/scriptedScene.ts @@ -1,4 +1,5 @@ import { + behaviors, EmbeddedScene, SceneControlsSpacer, SceneFlexItem, @@ -9,21 +10,19 @@ import { SceneVariableSet, VariableValueSelectors, } from '@grafana/scenes'; +import { DashboardCursorSync } from '@grafana/schema'; import { Check, CheckType, DashboardSceneAppConfig } from 'types'; import { getReachabilityStat, getUptimeStat, getVariables } from 'scenes/Common'; import { getAllLogs } from 'scenes/Common/allLogs'; import { getEditButton } from 'scenes/Common/editButton'; import { getEmptyScene } from 'scenes/Common/emptyScene'; -import { getAssertionLogsPanel } from 'scenes/MULTIHTTP/assertionLogs'; -import { getAssertionTable } from 'scenes/MULTIHTTP/assertionTable'; import { getDistinctTargets } from 'scenes/MULTIHTTP/distinctTargets'; import { getProbeDuration } from 'scenes/MULTIHTTP/probeDuration'; +import { getResultsByTargetTable } from './ResultsByTargetTable/ResultByTargetTable'; +import { getAssertionTable } from './AssertionsTable'; import { getDataTransferred } from './dataTransferred'; -import { getExpectedResponse } from './expectedResponse'; -import { getSuccessRateByUrl } from './successRateByUrl'; -import { getTimingByTarget } from './timingByTarget'; export function getScriptedScene({ metrics, logs }: DashboardSceneAppConfig, checks: Check[] = []) { return () => { @@ -48,6 +47,7 @@ export function getScriptedScene({ metrics, logs }: DashboardSceneAppConfig, che return new EmbeddedScene({ $timeRange: timeRange, $variables: variables, + $behaviors: [new behaviors.CursorSync({ key: 'sync', sync: DashboardCursorSync.Crosshair })], controls: [ new VariableValueSelectors({}), new SceneControlsSpacer(), @@ -65,34 +65,37 @@ export function getScriptedScene({ metrics, logs }: DashboardSceneAppConfig, che new SceneFlexLayout({ direction: 'row', height: 150, - children: [new SceneFlexItem({ body: uptime }), new SceneFlexItem({ body: reachability }), distinctTargets], + children: [new SceneFlexItem({ body: uptime }), new SceneFlexItem({ body: reachability })], }), new SceneFlexLayout({ direction: 'row', - height: 200, - children: [probeDuration], + children: [getAssertionTable(logs)], }), - getDataTransferred(metrics), new SceneFlexLayout({ direction: 'row', height: 200, - children: [getTimingByTarget(metrics)], - }), - new SceneFlexLayout({ - direction: 'row', - height: 400, - children: [getExpectedResponse(metrics)], - }), - new SceneFlexLayout({ - direction: 'row', - height: 200, - children: [getSuccessRateByUrl(metrics)], + children: [distinctTargets, probeDuration], }), + getDataTransferred(metrics), new SceneFlexLayout({ direction: 'row', - minHeight: 300, - children: [getAssertionTable(logs), getAssertionLogsPanel(logs)], + children: [getResultsByTargetTable(metrics)], }), + // new SceneFlexLayout({ + // direction: 'row', + // height: 400, + // children: [getExpectedResponse(metrics)], + // }), + // new SceneFlexLayout({ + // direction: 'row', + // height: 200, + // children: [getSuccessRateByUrl(metrics)], + // }), + // new SceneFlexLayout({ + // direction: 'row', + // minHeight: 300, + // children: [getAssertionLogsPanel(logs)], + // }), new SceneFlexLayout({ direction: 'row', minHeight: 300, diff --git a/yarn.lock b/yarn.lock index 71c4a53ed..37422e03e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12600,6 +12600,13 @@ react-custom-scrollbars@^4.2.1: prop-types "^15.5.10" raf "^3.1.0" +react-data-table-component@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/react-data-table-component/-/react-data-table-component-7.5.4.tgz#82bcf828139f7a88464cd4f5f88b3bc6ba676b29" + integrity sha512-6DGVj3urJZfEEMuP652fSjxdRVKeyb+9d0YounVc+MX8jwoyXQW6KO10eyZqElE9QtVrKrCeJxR7vht9yxyJiw== + dependencies: + deepmerge "^4.2.2" + react-dom@18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"