From d21d498142a70b128f6f15277b2bc8eef9b24cb6 Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 10 Mar 2023 05:35:39 -0300 Subject: [PATCH 01/33] Docs: Remove Alertmanager configuration rollback from What's New (#64564) * Docs: Remove Alertmanager configuration rollback from What's New * Trigger Build --------- Co-authored-by: Jack Baldry --- docs/sources/whatsnew/whats-new-in-v9-4.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/sources/whatsnew/whats-new-in-v9-4.md b/docs/sources/whatsnew/whats-new-in-v9-4.md index a7a980be37f..d47d7a20c23 100644 --- a/docs/sources/whatsnew/whats-new-in-v9-4.md +++ b/docs/sources/whatsnew/whats-new-in-v9-4.md @@ -254,12 +254,6 @@ We've added Discord as a contact point receiver for Grafana Cloud alert rules. We've made the following changes to alert administration. -#### Better guidance to configure your Alertmanagers - -Get additional help while configuring your Alertmanager. If you enter an invalid Alertmanager configuration, an error message displays, and you can choose from a previous working configuration to restart it. - -{{< figure src="/media/docs/alerting/alertmanager-config.png" max-width="750px" caption="Better guidance to configure your Alertmanager" >}} - #### Alerting landing page Introduces a new landing page that helps you get started quickly with Alerting. It also provides you with at a glance information on how Alerting works and a video to introduce you to key concepts. From 6cbc956b5cfad6a2eb47fca02d136a63c5ff138b Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Fri, 10 Mar 2023 09:58:29 +0100 Subject: [PATCH 02/33] LogContext: Fix border radius to be consistent (#64589) fix border radius in LogRowContext --- public/app/features/logs/components/LogRowContext.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/logs/components/LogRowContext.tsx b/public/app/features/logs/components/LogRowContext.tsx index 66eb5b60670..cbb683455cf 100644 --- a/public/app/features/logs/components/LogRowContext.tsx +++ b/public/app/features/logs/components/LogRowContext.tsx @@ -98,7 +98,7 @@ const getLogRowContextStyles = (theme: GrafanaTheme2, wrapLogMessage?: boolean, background: ${theme.colors.background.canvas}; `, top: css` - border-radius: 0 0 ${theme.shape.borderRadius(2)} ${theme.shape.borderRadius(2)}; + border-radius: 0 0 ${theme.shape.borderRadius()} ${theme.shape.borderRadius()}; box-shadow: 0 0 ${theme.spacing(1.25)} ${theme.v1.palette.black}; clip-path: inset(0px -${theme.spacing(1.25)} -${theme.spacing(1.25)} -${theme.spacing(1.25)}); `, @@ -110,7 +110,7 @@ const getLogRowContextStyles = (theme: GrafanaTheme2, wrapLogMessage?: boolean, height: ${headerHeight}px; background: ${theme.colors.background.secondary}; border: 1px solid ${theme.colors.background.secondary}; - border-radius: ${theme.shape.borderRadius(2)} ${theme.shape.borderRadius(2)} 0 0; + border-radius: ${theme.shape.borderRadius()} ${theme.shape.borderRadius()} 0 0; box-shadow: 0 0 ${theme.spacing(1.25)} ${theme.v1.palette.black}; clip-path: inset(-${theme.spacing(1.25)} -${theme.spacing(1.25)} 0px -${theme.spacing(1.25)}); font-family: ${theme.typography.fontFamily}; From 1667ea118ffc32a4cf953115b6ce0e2a5d647120 Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Fri, 10 Mar 2023 11:11:53 +0000 Subject: [PATCH 03/33] Trace View: Copy Trace ID action button (#64416) * Added button to trace view to copy trace ID * Added dummy Export button --- .../TracePageHeader/Actions/ActionButton.tsx | 59 +++++++++++++++++++ .../Actions/TracePageActions.tsx | 51 ++++++++++++++++ .../TracePageHeader/NewTracePageHeader.tsx | 23 ++++++-- 3 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 public/app/features/explore/TraceView/components/TracePageHeader/Actions/ActionButton.tsx create mode 100644 public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/Actions/ActionButton.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/ActionButton.tsx new file mode 100644 index 00000000000..8be7be68cd4 --- /dev/null +++ b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/ActionButton.tsx @@ -0,0 +1,59 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2, IconName } from '@grafana/data'; +import { Button, useStyles2 } from '@grafana/ui'; + +export const getStyles = (theme: GrafanaTheme2) => { + return { + ActionButton: css` + label: ActionButton; + overflow: hidden; + position: relative; + width: 110px; + justify-content: center; + &:after { + content: ''; + background: ${theme.colors.primary.main}; + display: block; + position: absolute; + right: 0; + width: 100%; + height: 100%; + opacity: 0; + transition: all 0.8s; + } + &:active:after { + margin: 0; + opacity: 0.3; + transition: 0s; + } + `, + }; +}; + +export type ActionButtonProps = { + onClick: () => void; + ariaLabel: string; + label: string; + icon: IconName; +}; + +export default function ActionButton(props: ActionButtonProps) { + const { onClick, ariaLabel, label, icon } = props; + const styles = useStyles2(getStyles); + + return ( + + ); +} diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx new file mode 100644 index 00000000000..7475fec0f99 --- /dev/null +++ b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx @@ -0,0 +1,51 @@ +import { css } from '@emotion/css'; +import React, { useState } from 'react'; + +import { useStyles2 } from '@grafana/ui'; + +import ActionButton from './ActionButton'; + +export const getStyles = () => { + return { + TracePageActions: css` + label: TracePageActions; + display: flex; + gap: 4px; + `, + }; +}; + +export type TracePageActionsProps = { + traceId: string; +}; + +export default function TracePageActions(props: TracePageActionsProps) { + const { traceId } = props; + const styles = useStyles2(getStyles); + const [copyTraceIdClicked, setCopyTraceIdClicked] = useState(false); + + const copyTraceId = () => { + navigator.clipboard.writeText(traceId); + setCopyTraceIdClicked(true); + setTimeout(() => { + setCopyTraceIdClicked(false); + }, 5000); + }; + + return ( +
+ + alert('not implemented')} + ariaLabel={'Export Trace'} + label={'Export'} + icon={'save'} + /> +
+ ); +} diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx index 1e4abc6b62b..99674c1c482 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx @@ -26,15 +26,29 @@ import { getTraceLinks } from '../model/link-patterns'; import { getHeaderTags, getTraceName } from '../model/trace-viewer'; import { formatDuration } from '../utils/date'; +import TracePageActions from './Actions/TracePageActions'; import SpanGraph from './SpanGraph'; import { TracePageHeaderEmbedProps, timestamp, getStyles } from './TracePageHeader'; const getNewStyles = (theme: GrafanaTheme2) => { return { + titleRow: css` + label: TracePageHeaderTitleRow; + align-items: center; + display: flex; + padding: 0 0.5em 0 0.5em; + `, + title: css` + label: TracePageHeaderTitle; + color: inherit; + flex: 1; + font-size: 1.7em; + line-height: 1em; + `, subtitle: css` flex: 1; line-height: 1em; - margin: -0.5em 0 1.5em 0.5em; + margin: -0.5em 0.5em 1.5em 0.5em; `, tag: css` margin: 0 0.5em 0 0; @@ -56,7 +70,7 @@ const getNewStyles = (theme: GrafanaTheme2) => { position: sticky; top: 0; z-index: 5; - padding: 10px 5px 0 5px; + padding: 0.5em 0.25em 0 0.25em; & > :last-child { border-bottom: 1px solid ${autoColor(theme, '#ccc')}; } @@ -82,7 +96,7 @@ export function NewTracePageHeader(props: TracePageHeaderEmbedProps) { const { method, status, url } = getHeaderTags(trace.spans); const title = ( -

+

| @@ -102,9 +116,10 @@ export function NewTracePageHeader(props: TracePageHeaderEmbedProps) { return (
-
+
{links && links.length > 0 && } {title} +
From 4c8855ed2dd97dd5dda63323c4e1d58919be928f Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Fri, 10 Mar 2023 12:20:28 +0100 Subject: [PATCH 04/33] Alerting: Fix contact point name being URL-encoded in the title (#64590) Fix contact point name being encoded in the title text --- public/app/features/alerting/unified/Receivers.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx index 0ed184c1d4a..6eff867f0f1 100644 --- a/public/app/features/alerting/unified/Receivers.tsx +++ b/public/app/features/alerting/unified/Receivers.tsx @@ -96,8 +96,7 @@ const Receivers = () => { const integrationsErrorCount = contactPointsState?.errorCount ?? 0; const disableAmSelect = !isRoot; - - let pageNav = getPageNavigationModel(type, id, isduplicatingTemplate); + let pageNav = getPageNavigationModel(type, id ? decodeURIComponent(id) : undefined, isduplicatingTemplate); if (!alertManagerSourceName) { return isRoot ? ( From af5ee9c66d9e6b2ee0e86ac1500266edb5781357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 10 Mar 2023 12:25:25 +0100 Subject: [PATCH 05/33] PanelChrome: Fixes title max width, and make make menu more prominent (#64492) * PanelChrome: Fixes title max width, and make make menu more prominent * Add fix for wrapping timeshift --- .../grafana-ui/src/components/PanelChrome/PanelChrome.tsx | 5 ++++- .../dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index 3ff8f3b0e2b..d6d0c40c2c7 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -285,7 +285,6 @@ const getStyles = (theme: GrafanaTheme2) => { textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', - maxWidth: theme.spacing(50), fontSize: theme.typography.h6.fontSize, fontWeight: theme.typography.h6.fontWeight, }), @@ -301,6 +300,10 @@ const getStyles = (theme: GrafanaTheme2) => { label: 'panel-menu', visibility: 'hidden', border: 'none', + background: theme.colors.secondary.main, + '&:hover': { + background: theme.colors.secondary.shade, + }, }), errorContainerFloating: css({ label: 'error-container', diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx index 8f43db0f959..64cc2795c96 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx @@ -74,6 +74,7 @@ const getStyles = (theme: GrafanaTheme2) => { timeshift: css({ color: theme.colors.text.link, gap: theme.spacing(0.5), + whiteSpace: 'nowrap', '&:hover': { color: theme.colors.emphasize(theme.colors.text.link, 0.03), From 3ff380f40f64f492ce017023aebf02fc7d7b0c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 10 Mar 2023 12:25:35 +0100 Subject: [PATCH 06/33] Alerting: Minor style fix for home page (#64585) * Minor fix for home alerting page * Minor border fix * Remove outline --- public/app/features/alerting/unified/Home.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/features/alerting/unified/Home.tsx b/public/app/features/alerting/unified/Home.tsx index 1001c74b4a5..bcb07444adc 100644 --- a/public/app/features/alerting/unified/Home.tsx +++ b/public/app/features/alerting/unified/Home.tsx @@ -250,8 +250,7 @@ const getContentBoxStyles = (theme: GrafanaTheme2) => ({ box: css` padding: ${theme.spacing(2)}; background-color: ${theme.colors.background.secondary}; - border-radius: 3px; - outline: 1px solid ${theme.colors.border.strong}; + border-radius: ${theme.shape.borderRadius()}; `, }); From 14251db9bad9846f88a6816f04681751a639431e Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Fri, 10 Mar 2023 05:49:02 -0600 Subject: [PATCH 07/33] Chore: uFuzzy 1.0.6 (#64575) --- package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../unified/hooks/useFilteredRules.ts | 6 +- .../app/features/search/service/frontend.ts | 2 +- .../components/MetricEncyclopediaModal.tsx | 78 ++++++++----------- .../components/FlameGraph/FlameGraph.tsx | 8 +- yarn.lock | 12 +-- 7 files changed, 49 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index 448494bac54..a696c38a518 100644 --- a/package.json +++ b/package.json @@ -271,7 +271,7 @@ "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", "@kusto/monaco-kusto": "5.3.6", - "@leeoniya/ufuzzy": "1.0.2", + "@leeoniya/ufuzzy": "1.0.6", "@lezer/common": "1.0.2", "@lezer/highlight": "1.1.3", "@lezer/lr": "1.3.3", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 8c229cc0eae..2ad949e3324 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -52,7 +52,7 @@ "@grafana/data": "9.5.0-pre", "@grafana/e2e-selectors": "9.5.0-pre", "@grafana/schema": "9.5.0-pre", - "@leeoniya/ufuzzy": "1.0.2", + "@leeoniya/ufuzzy": "1.0.6", "@monaco-editor/react": "4.4.6", "@popperjs/core": "2.11.6", "@react-aria/button": "3.6.1", diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.ts index f7b259706a6..75cc2226bee 100644 --- a/public/app/features/alerting/unified/hooks/useFilteredRules.ts +++ b/public/app/features/alerting/unified/hooks/useFilteredRules.ts @@ -108,7 +108,7 @@ export const filterRules = ( const [idxs, info, order] = ufuzzy.search(namespaceHaystack, namespaceFilter); if (info && order) { filteredNamespaces = order.map((idx) => filteredNamespaces[info.idx[idx]]); - } else { + } else if (idxs) { filteredNamespaces = idxs.map((idx) => filteredNamespaces[idx]); } } @@ -127,7 +127,7 @@ const reduceNamespaces = (filterState: RulesFilter) => { const [idxs, info, order] = ufuzzy.search(groupsHaystack, groupNameFilter); if (info && order) { filteredGroups = order.map((idx) => filteredGroups[info.idx[idx]]); - } else { + } else if (idxs) { filteredGroups = idxs.map((idx) => filteredGroups[idx]); } } @@ -157,7 +157,7 @@ const reduceGroups = (filterState: RulesFilter) => { const [idxs, info, order] = ufuzzy.search(rulesHaystack, ruleNameQuery); if (info && order) { filteredRules = order.map((idx) => filteredRules[info.idx[idx]]); - } else { + } else if (idxs) { filteredRules = idxs.map((idx) => filteredRules[idx]); } } diff --git a/public/app/features/search/service/frontend.ts b/public/app/features/search/service/frontend.ts index b11fe34d77d..51b5af4c657 100644 --- a/public/app/features/search/service/frontend.ts +++ b/public/app/features/search/service/frontend.ts @@ -130,7 +130,7 @@ class FullResultCache { } } // > 1000 matches (unranked) - else { + else if (idxs) { for (let i = 0; i < idxs.length; i++) { let haystackIdx = idxs[i]; dst.push(src[haystackIdx]); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.tsx index 969a6135a4a..60e5a6dcb3a 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import uFuzzy from '@leeoniya/ufuzzy'; import debounce from 'debounce-promise'; import { debounce as debounceLodash } from 'lodash'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; @@ -81,9 +81,24 @@ export const placeholders = { export const DEFAULT_RESULTS_PER_PAGE = 10; -export const MetricEncyclopediaModal = (props: Props) => { - const uf = UseUfuzzy(); +const uf = new uFuzzy({ + intraMode: 1, + intraIns: 1, + intraSub: 1, + intraTrn: 1, + intraDel: 1, +}); +function fuzzySearch(haystack: string[], query: string, setter: React.Dispatch>) { + // console.log('fuzzySearch'); + + const idxs = uf.filter(haystack, query); + idxs && setter(idxs); +} + +const debouncedFuzzySearch = debounceLodash(fuzzySearch, 300); + +export const MetricEncyclopediaModal = (props: Props) => { const { datasource, isOpen, onClose, onChange, query } = props; const [variables, setVariables] = useState>>([]); @@ -93,7 +108,7 @@ export const MetricEncyclopediaModal = (props: Props) => { // metric list const [metrics, setMetrics] = useState([]); const [hasMetadata, setHasMetadata] = useState(true); - const [haystack, setHaystack] = useState([]); + const [metaHaystack, setMetaHaystack] = useState([]); const [nameHaystack, setNameHaystack] = useState([]); const [openTabs, setOpenTabs] = useState([]); @@ -104,7 +119,7 @@ export const MetricEncyclopediaModal = (props: Props) => { // filters const [fuzzySearchQuery, setFuzzySearchQuery] = useState(''); const [fuzzyMetaSearchResults, setFuzzyMetaSearchResults] = useState([]); - const [fuzzyNameSearchResults, setNameFuzzySearchResults] = useState([]); + const [fuzzyNameSearchResults, setFuzzyNameSearchResults] = useState([]); const [fullMetaSearch, setFullMetaSearch] = useState(false); const [excludeNullMetadata, setExcludeNullMetadata] = useState(false); const [selectedTypes, setSelectedTypes] = useState>>([]); @@ -141,14 +156,14 @@ export const MetricEncyclopediaModal = (props: Props) => { metrics = (await datasource.languageProvider.getLabelValues('__name__')) ?? []; } - let haystackData: string[] = []; + let haystackMetaData: string[] = []; let haystackNameData: string[] = []; let metricsData: MetricsData = metrics.map((m) => { const type = getMetadataType(m, datasource.languageProvider.metricsMetadata!); const description = getMetadataHelp(m, datasource.languageProvider.metricsMetadata!); // string[] = name + type + description - haystackData.push(`${m} ${type} ${description}`); + haystackMetaData.push(`${m} ${type} ${description}`); haystackNameData.push(m); return { value: m, @@ -159,7 +174,7 @@ export const MetricEncyclopediaModal = (props: Props) => { // setting this by the backend if useBackend is true setMetrics(metricsData); - setHaystack(haystackData); + setMetaHaystack(haystackMetaData); setNameHaystack(haystackNameData); setVariables( @@ -211,27 +226,6 @@ export const MetricEncyclopediaModal = (props: Props) => { return selectedTypes.length > 0; } - function fuzzySearch(query: string) { - // search either the names or all metadata - // fuzzy search go! - - if (fullMetaSearch) { - // considered simply filtering indexes with reduce and includes - // Performance comparison with 13,000 metrics searching metadata - // Fuzzy 6326ms - // Reduce & Includes 5541ms - const metaIdxs = uf.filter(haystack, query.toLowerCase()); - setFuzzyMetaSearchResults(metaIdxs); - } else { - const nameIdxs = uf.filter(nameHaystack, query.toLowerCase()); - setNameFuzzySearchResults(nameIdxs); - } - } - - const debouncedFuzzySearch = debounceLodash((query: string) => { - fuzzySearch(query); - }, 300); - /** * Filter * @@ -367,8 +361,14 @@ export const MetricEncyclopediaModal = (props: Props) => { setIsLoading(true); debouncedBackendSearch(value); } else { - // do the search on the frontend - debouncedFuzzySearch(value); + // search either the names or all metadata + // fuzzy search go! + + if (fullMetaSearch) { + debouncedFuzzySearch(metaHaystack, value, setFuzzyMetaSearchResults); + } else { + debouncedFuzzySearch(nameHaystack, value, setFuzzyNameSearchResults); + } } setPageNum(1); @@ -656,22 +656,6 @@ function alphabetically(ascending: boolean, metadataFilters: boolean) { }; } -function UseUfuzzy(): uFuzzy { - const ref = useRef(); - - if (!ref.current) { - ref.current = new uFuzzy({ - intraMode: 1, - intraIns: 1, - intraSub: 1, - intraTrn: 1, - intraDel: 1, - }); - } - - return ref.current; -} - const getStyles = (theme: GrafanaTheme2) => { return { cardsContainer: css` diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.tsx b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.tsx index 22d150e2463..c72381de55a 100644 --- a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.tsx +++ b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.tsx @@ -92,8 +92,12 @@ const FlameGraph = ({ const foundLabels = new Set(); if (search) { - for (let idx of ufuzzy.filter(uniqueLabels, search)) { - foundLabels.add(uniqueLabels[idx]); + let idxs = ufuzzy.filter(uniqueLabels, search); + + if (idxs) { + for (let idx of idxs) { + foundLabels.add(uniqueLabels[idx]); + } } } diff --git a/yarn.lock b/yarn.lock index 97eb18e03a8..b72d883bfb7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5351,7 +5351,7 @@ __metadata: "@grafana/e2e-selectors": 9.5.0-pre "@grafana/schema": 9.5.0-pre "@grafana/tsconfig": ^1.2.0-rc1 - "@leeoniya/ufuzzy": 1.0.2 + "@leeoniya/ufuzzy": 1.0.6 "@mdx-js/react": 1.6.22 "@monaco-editor/react": 4.4.6 "@popperjs/core": 2.11.6 @@ -6330,10 +6330,10 @@ __metadata: languageName: node linkType: hard -"@leeoniya/ufuzzy@npm:1.0.2": - version: 1.0.2 - resolution: "@leeoniya/ufuzzy@npm:1.0.2" - checksum: 5460378a8c32d121b0bc7c8e95cde995316516655528e248051b1bf360cdca0311ef3275de14b802587748231333cee6183c931b3abba26f9e4236ecc4959aa3 +"@leeoniya/ufuzzy@npm:1.0.6": + version: 1.0.6 + resolution: "@leeoniya/ufuzzy@npm:1.0.6" + checksum: e09672848e094745726331feebe6744d42563ccf160b38796eed4d72105daa052838fdec1944d5b44a27e328fc74a00547d00b894710e2720aa692ed5d3d6e3b languageName: node linkType: hard @@ -22136,7 +22136,7 @@ __metadata: "@grafana/tsconfig": ^1.2.0-rc1 "@grafana/ui": "workspace:*" "@kusto/monaco-kusto": 5.3.6 - "@leeoniya/ufuzzy": 1.0.2 + "@leeoniya/ufuzzy": 1.0.6 "@lezer/common": 1.0.2 "@lezer/highlight": 1.1.3 "@lezer/lr": 1.3.3 From f1a17d54cdd59c421c88877b39a9748427ea9bb0 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Fri, 10 Mar 2023 12:56:41 +0100 Subject: [PATCH 08/33] PanelHeader: Add analytics (#64533) --- .../app/features/dashboard/utils/getPanelMenu.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index 8f2a2610a7a..339281c4c45 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -43,6 +43,7 @@ export function getPanelMenu( locationService.partial({ viewPanel: panel.id, }); + reportInteraction('dashboards_panelheader_view_clicked'); }; const onEditPanel = (event: React.MouseEvent) => { @@ -50,21 +51,25 @@ export function getPanelMenu( locationService.partial({ editPanel: panel.id, }); + reportInteraction('dashboards_panelheader_edit_clicked'); }; const onSharePanel = (event: React.MouseEvent) => { event.preventDefault(); sharePanel(dashboard, panel); + reportInteraction('dashboards_panelheader_share_clicked'); }; const onAddLibraryPanel = (event: React.MouseEvent) => { event.preventDefault(); addLibraryPanel(dashboard, panel); + reportInteraction('dashboards_panelheader_createlibrarypanel_clicked'); }; const onUnlinkLibraryPanel = (event: React.MouseEvent) => { event.preventDefault(); unlinkLibraryPanel(panel); + reportInteraction('dashboards_panelheader_unlinklibrarypanel_clicked'); }; const onInspectPanel = (tab?: InspectTab) => { @@ -72,10 +77,7 @@ export function getPanelMenu( inspect: panel.id, inspectTab: tab, }); - - reportInteraction('grafana_panel_menu_inspect', { - tab: tab ?? InspectTab.Data, - }); + reportInteraction('dashboards_panelheader_inspect_clicked', { tab: tab ?? InspectTab.Data }); }; const onMore = (event: React.MouseEvent) => { @@ -85,16 +87,19 @@ export function getPanelMenu( const onDuplicatePanel = (event: React.MouseEvent) => { event.preventDefault(); duplicatePanel(dashboard, panel); + reportInteraction('dashboards_panelheader_duplicate_clicked'); }; const onCopyPanel = (event: React.MouseEvent) => { event.preventDefault(); copyPanel(panel); + reportInteraction('dashboards_panelheader_copy_clicked'); }; const onRemovePanel = (event: React.MouseEvent) => { event.preventDefault(); removePanel(dashboard, panel, true); + reportInteraction('dashboards_panelheader_remove_clicked'); }; const onNavigateToExplore = (event: React.MouseEvent) => { @@ -102,16 +107,19 @@ export function getPanelMenu( const openInNewWindow = event.ctrlKey || event.metaKey ? (url: string) => window.open(`${config.appSubUrl}${url}`) : undefined; store.dispatch(navigateToExplore(panel, { getDataSourceSrv, getTimeSrv, getExploreUrl, openInNewWindow }) as any); + reportInteraction('dashboards_panelheader_explore_clicked'); }; const onToggleLegend = (event: React.MouseEvent) => { event.preventDefault(); toggleLegend(panel); + reportInteraction('dashboards_panelheader_togglelegend_clicked'); }; const onCancelStreaming = (event: React.MouseEvent) => { event.preventDefault(); panel.getQueryRunner().cancelQuery(); + reportInteraction('dashboards_panelheader_cancelstreaming_clicked'); }; const menu: PanelMenuItem[] = []; From b46771cbfe1e7561b44133e7f375aafbe28930dc Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Fri, 10 Mar 2023 13:13:51 +0100 Subject: [PATCH 09/33] Loki: Always fetch for new label keys in the QueryBuilder (#64597) * always fetch new labels * remove refreshLogLabels --- public/app/plugins/datasource/loki/LanguageProvider.ts | 6 ------ .../loki/querybuilder/components/LokiQueryBuilder.tsx | 3 +-- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/public/app/plugins/datasource/loki/LanguageProvider.ts b/public/app/plugins/datasource/loki/LanguageProvider.ts index 3e27c5f87ba..637bf49a2bc 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.ts @@ -386,12 +386,6 @@ export default class LokiLanguageProvider extends LanguageProvider { return []; } - async refreshLogLabels(forceRefresh?: boolean) { - if ((this.labelKeys && Date.now().valueOf() - this.labelFetchTs > LABEL_REFRESH_INTERVAL) || forceRefresh) { - await this.fetchLabels(); - } - } - /** * Fetch labels for a selector. This is cached by its args but also by the global timeRange currently selected as * they can change over requested time. diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx index 9ff76b04b3f..0e92e3cd358 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx @@ -49,8 +49,7 @@ export const LokiQueryBuilder = React.memo(({ datasource, query, onChange const labelsToConsider = query.labels.filter((x) => x !== forLabel); if (labelsToConsider.length === 0) { - await datasource.languageProvider.refreshLogLabels(); - return datasource.languageProvider.getLabelKeys(); + return await datasource.languageProvider.fetchLabels(); } const expr = lokiQueryModeller.renderLabels(labelsToConsider); From c955c20670e4723592ba52bce67fa56ad10ea1a7 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 10 Mar 2023 12:15:41 +0000 Subject: [PATCH 10/33] Chore: assign ownership of tracing feature flag to user essentials (#64598) assign ownership of tracing feature flag to user essentials --- pkg/services/featuremgmt/registry.go | 1 + pkg/services/featuremgmt/toggles_gen_test.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 46a822cdba4..cacfb8a7864 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -151,6 +151,7 @@ var ( Description: "Adds trace ID to error notifications", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaUserEssentialsSquad, }, { Name: "newTraceView", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 03a329238e1..85caf10901e 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -43,7 +43,6 @@ func TestFeatureToggleFiles(t *testing.T) { ownerlessFeatures := map[string]bool{ "prometheusAzureOverrideAudience": true, - "tracing": true, } t.Run("all new features should have an owner", func(t *testing.T) { From 548a5054ad8fa5c199bd01e20a02fbbe558d52fe Mon Sep 17 00:00:00 2001 From: Domas Date: Fri, 10 Mar 2023 14:41:06 +0200 Subject: [PATCH 11/33] Table: Introduce sparkline cell type (#63182) --- .../panel-table/table_tests_new.json | 1766 ++++++++++------- .../tablepanelcfg/schema-reference.md | 1 + .../transform-data/index.md | 6 + .../visualizations/table/index.md | 8 + .../feature-toggles/index.md | 1 + packages/grafana-data/src/dataframe/index.ts | 2 +- packages/grafana-data/src/dataframe/utils.ts | 6 +- packages/grafana-data/src/field/index.ts | 2 +- .../src/transformations/transformers/ids.ts | 1 + .../transformers/seriesToRows.ts | 4 +- packages/grafana-data/src/types/dataFrame.ts | 1 + .../src/types/featureToggles.gen.ts | 1 + .../grafana-schema/src/common/common.gen.ts | 21 +- .../grafana-schema/src/common/mudball.cue | 3 + packages/grafana-schema/src/common/table.cue | 10 +- .../src/components/Sparkline/Sparkline.tsx | 7 +- .../src/components/Table/FooterRow.tsx | 4 +- .../src/components/Table/SparklineCell.tsx | 114 ++ .../grafana-ui/src/components/Table/Table.tsx | 10 +- .../grafana-ui/src/components/Table/styles.ts | 37 +- .../grafana-ui/src/components/Table/types.ts | 2 + .../grafana-ui/src/components/Table/utils.ts | 15 + pkg/services/featuremgmt/codeowners.go | 1 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.go | 4 + .../components/expressions/Expression.tsx | 5 +- .../unified/components/rule-editor/util.ts | 5 +- .../page/components/SearchResultsTable.tsx | 7 +- .../transformers/standardTransformers.ts | 3 + .../TimeSeriesTableTransformEditor.tsx | 25 + .../timeSeriesTableTransformer.test.ts | 104 + .../timeSeriesTableTransformer.ts | 127 ++ .../panel/table/TableCellOptionEditor.tsx | 11 + public/app/plugins/panel/table/TablePanel.tsx | 1 + .../cells/SparklineCellOptionsEditor.tsx | 79 + public/app/plugins/panel/table/module.tsx | 20 +- public/app/plugins/panel/table/panelcfg.cue | 2 + .../app/plugins/panel/table/panelcfg.gen.ts | 5 + 38 files changed, 1714 insertions(+), 714 deletions(-) create mode 100644 packages/grafana-ui/src/components/Table/SparklineCell.tsx create mode 100644 public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx create mode 100644 public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.test.ts create mode 100644 public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts create mode 100644 public/app/plugins/panel/table/cells/SparklineCellOptionsEditor.tsx diff --git a/devenv/dev-dashboards/panel-table/table_tests_new.json b/devenv/dev-dashboards/panel-table/table_tests_new.json index 11c6c8c0eb8..a373c5080b3 100644 --- a/devenv/dev-dashboards/panel-table/table_tests_new.json +++ b/devenv/dev-dashboards/panel-table/table_tests_new.json @@ -1,688 +1,1100 @@ { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "gnetId": null, - "graphTooltip": 0, - "links": [], - "panels": [ - { - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 7, - "title": "Cell styles", - "type": "row" + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] }, - { - "datasource": "gdev-testdata", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "center", - "displayMode": "color-background", - "filterable": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "blue", - "value": 20 - }, - { - "color": "orange", - "value": 60 - }, - { - "color": "red", - "value": 70 - } - ] - }, - "unit": "degree" + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 7, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Cell styles", + "type": "row" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Max" - }, - "properties": [ - { - "id": "custom.width", - "value": 84 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Last" - }, - "properties": [ - { - "id": "custom.width", - "value": 78 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Mean" - }, - "properties": [ - { - "id": "custom.width", - "value": 74 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Field" - }, - "properties": [ - { - "id": "custom.align", - "value": "left" - } - ] - } - ] - }, - "gridPos": { - "h": 16, - "w": 7, - "x": 0, - "y": 1 - }, - "id": 4, - "options": { - "showHeader": true, - "sortBy": [ - { - "desc": true, - "displayName": "Last" - } - ] - }, - "pluginVersion": "", - "targets": [ { - "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 15, - "stringInput": "" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Colored background", - "transformations": [ - { - "id": "reduce", - "options": { - "reducers": [ - "max", - "mean", - "last" - ] - } - } - ], - "type": "table" - }, - { - "datasource": "gdev-testdata", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": null, - "filterable": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "orange", - "value": null - }, - { - "color": "red", - "value": 50 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "A" + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" }, - "properties": [ - { - "id": "custom.displayMode", - "value": "gradient-gauge" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Info" - }, - "properties": [ - { - "id": "custom.width", - "value": 92 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Min" - }, - "properties": [ - { - "id": "custom.width", - "value": 76 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Max" - }, - "properties": [ - { - "id": "custom.width", - "value": 89 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "custom.width", - "value": 165 - } - ] - } - ] - }, - "gridPos": { - "h": 16, - "w": 8, - "x": 7, - "y": 1 - }, - "id": 2, - "options": { - "showHeader": true, - "sortBy": [ - { - "desc": false, - "displayName": "Min" - } - ] - }, - "pluginVersion": "", - "targets": [ - { - "refId": "A", - "scenarioId": "random_walk_table", - "stringInput": "" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Bar gauge cells", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Time": true - }, - "indexByName": { - "Info": 1, - "Max": 3, - "Min": 2, - "Time": 0, - "Value": 4 - }, - "renameByName": {} - } - } - ], - "type": "table" - }, - { - "datasource": "gdev-testdata", - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": null, - "filterable": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "blue", - "value": null - }, - { - "color": "green", - "value": 50 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "A" - }, - "properties": [ - { - "id": "custom.displayMode", - "value": "lcd-gauge" - }, - { - "id": "custom.align", - "value": "center" - } - ] - } - ] - }, - "gridPos": { - "h": 16, - "w": 9, - "x": 15, - "y": 1 - }, - "id": 5, - "options": { - "showHeader": true, - "sortBy": [] - }, - "pluginVersion": "", - "targets": [ - { - "refId": "A", - "scenarioId": "random_walk_table", - "stringInput": "" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Retro LCD cell", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Info": false, - "Max": true, - "Min": true, - "Time": false - }, - "indexByName": { - "Info": 1, - "Max": 3, - "Min": 2, - "Time": 0, - "Value": 4 - }, - "renameByName": {} - } - } - ], - "type": "table" - }, - { - "collapsed": false, - "datasource": "gdev-testdata", - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 17 - }, - "id": 9, - "panels": [], - "title": "Data links", - "type": "row" - }, - { - "datasource": "gdev-testdata", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "center", - "displayMode": "color-text", - "filterable": false - }, - "decimals": 2, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "blue", - "value": 20 - }, - { - "color": "orange", - "value": 50 - }, - { - "color": "red", - "value": 70 - } - ] - }, - "unit": "percent" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "custom.align", - "value": null - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "{name=\"S1\", server=\"A\"}" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Details", - "url": "http://detail?serverLabel=${__field.labels.server}&valueNumeric=${__value.numeric}" - } + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "mode": "gradient", + "type": "color-background" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "blue", + "value": 20 + }, + { + "color": "orange", + "value": 60 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "degree" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Max" + }, + "properties": [ + { + "id": "custom.width", + "value": 84 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Last" + }, + "properties": [ + { + "id": "custom.width", + "value": 78 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mean" + }, + "properties": [ + { + "id": "custom.width", + "value": 74 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Field" + }, + "properties": [ + { + "id": "custom.align", + "value": "left" + } + ] + } ] - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 18 - }, - "id": 3, - "maxDataPoints": "10", - "options": { - "showHeader": true - }, - "pluginVersion": "", - "targets": [ - { - "alias": "S1", - "labels": "server=A", - "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 1, - "stringInput": "" - }, - { - "alias": "S2", - "labels": "server=B", - "refId": "B", - "scenarioId": "random_walk", - "seriesCount": 1, - "stringInput": "" - }, - { - "alias": "S3", - "labels": "server=C", - "refId": "C", - "scenarioId": "random_walk", - "seriesCount": 1, - "stringInput": "" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Data link with labels and numeric value", - "transformations": [ - { - "id": "seriesToColumns", - "options": {} - } - ], - "type": "table" - }, - { - "datasource": "gdev-testdata", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "center", - "displayMode": "auto", - "filterable": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "blue", - "value": 20 - }, - { - "color": "orange", - "value": 60 - }, - { - "color": "red", - "value": 70 - } - ] - }, - "unit": "degree" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 5, - "x": 12, - "y": 18 - }, - "id": 10, - "options": { - "showHeader": false, - "sortBy": [ - { - "desc": true, - "displayName": "Last" - } - ] - }, - "pluginVersion": "", - "targets": [ - { - "refId": "A", - "scenarioId": "random_walk_table", - "seriesCount": 5, - "stringInput": "" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "No header", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Min": true, - "Time": true, - "Value": true }, - "indexByName": { - "Info": 2, - "Max": 4, - "Min": 3, - "Time": 0, - "Value": 1 + "gridPos": { + "h": 16, + "w": 7, + "x": 0, + "y": 1 }, - "renameByName": {} - } + "id": 4, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false, + "sortBy": [ + { + "desc": true, + "displayName": "Last" + } + ] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 15, + "stringInput": "" + } + ], + "title": "Colored background", + "transformations": [ + { + "id": "reduce", + "options": { + "reducers": [ + "max", + "mean", + "last" + ] + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "orange" + }, + { + "color": "red", + "value": 50 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Info" + }, + "properties": [ + { + "id": "custom.width", + "value": 92 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Min" + }, + "properties": [ + { + "id": "custom.width", + "value": 76 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Max" + }, + "properties": [ + { + "id": "custom.width", + "value": 89 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.width", + "value": 165 + } + ] + } + ] + }, + "gridPos": { + "h": 16, + "w": 8, + "x": 7, + "y": 1 + }, + "id": 2, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false, + "sortBy": [ + { + "desc": false, + "displayName": "Min" + } + ] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A", + "scenarioId": "random_walk_table", + "stringInput": "" + } + ], + "title": "Bar gauge cells", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": { + "Info": 1, + "Max": 3, + "Min": 2, + "Time": 0, + "Value": 4 + }, + "renameByName": {} + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "blue" + }, + { + "color": "green", + "value": 50 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "lcd", + "type": "gauge" + } + }, + { + "id": "custom.align", + "value": "center" + } + ] + } + ] + }, + "gridPos": { + "h": 16, + "w": 9, + "x": 15, + "y": 1 + }, + "id": 5, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false, + "sortBy": [] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A", + "scenarioId": "random_walk_table", + "stringInput": "" + } + ], + "title": "Retro LCD cell", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Info": false, + "Max": true, + "Min": true, + "Time": false + }, + "indexByName": { + "Info": 1, + "Max": 3, + "Min": 2, + "Time": 0, + "Value": 4 + }, + "renameByName": {} + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "rate" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "sparkline" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 9, + "x": 0, + "y": 17 + }, + "id": 14, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "cluster=eu,service=checkout", + "min": 0.1, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 3 + } + ], + "title": "Sparkline cell", + "transformations": [ + { + "id": "timeSeriesTable", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "indexByName": {}, + "renameByName": { + "Trend": "rate" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "rate" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "drawStyle": "bars", + "fillOpacity": 40, + "gradientMode": "opacity", + "lineWidth": 1, + "showPoints": "auto", + "type": "sparkline" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "latency" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "fillOpacity": 40, + "gradientMode": "hue", + "type": "sparkline" + } + }, + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 9, + "x": 9, + "y": 17 + }, + "id": 15, + "maxDataPoints": 100, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "cluster=eu,service=checkout", + "min": 0.1, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 3 + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "hide": false, + "labels": "cluster=eu,service=checkout", + "min": 0.1, + "refId": "B", + "scenarioId": "random_walk", + "seriesCount": 3 + } + ], + "title": "Multiple sparkline cells per row", + "transformations": [ + { + "id": "timeSeriesTable", + "options": {} + }, + { + "id": "joinByField", + "options": { + "byField": "service", + "mode": "outer" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "cluster 2": true + }, + "indexByName": {}, + "renameByName": { + "Trend": "rate", + "Trend #A": "rate", + "Trend #B": "latency", + "cluster 1": "cluster" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 25 + }, + "id": 9, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A" + } + ], + "title": "Data links", + "type": "row" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "color-text" + }, + "filterable": false, + "inspect": false + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "blue", + "value": 20 + }, + { + "color": "orange", + "value": 50 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "percent" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.align" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "{name=\"S1\", server=\"A\"}" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "title": "Details", + "url": "http://detail?serverLabel=${__field.labels.server}&valueNumeric=${__value.numeric}" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 26 + }, + "id": 3, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "alias": "S1", + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "server=A", + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 1, + "stringInput": "" + }, + { + "alias": "S2", + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "server=B", + "refId": "B", + "scenarioId": "random_walk", + "seriesCount": 1, + "stringInput": "" + }, + { + "alias": "S3", + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "server=C", + "refId": "C", + "scenarioId": "random_walk", + "seriesCount": 1, + "stringInput": "" + } + ], + "title": "Data link with labels and numeric value", + "transformations": [ + { + "id": "seriesToColumns", + "options": {} + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "blue", + "value": 20 + }, + { + "color": "orange", + "value": 60 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "degree" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 5, + "x": 12, + "y": 26 + }, + "id": 10, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": false, + "showRowNums": false, + "sortBy": [ + { + "desc": true, + "displayName": "Last" + } + ] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A", + "scenarioId": "random_walk_table", + "seriesCount": 5, + "stringInput": "" + } + ], + "title": "No header", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Min": true, + "Time": true, + "Value": true + }, + "indexByName": { + "Info": 2, + "Max": 4, + "Min": 3, + "Time": 0, + "Value": 1 + }, + "renameByName": {} + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 34 + }, + "id": 12, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true + }, + "pluginVersion": "9.4.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A" + } + ], + "title": "Footer", + "type": "table" } - ], - "type": "table" + ], + "refresh": "", + "revision": 1, + "schemaVersion": 38, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [] }, - { - "datasource": "gdev-testdata", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "displayMode": "auto" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 26 - }, - "id": 12, - "options": { - "footer": { - "show": true, - "fields": "", - "reducer": [ - "sum" - ] - }, - "showHeader": true - }, - "pluginVersion": "", - "title": "Footer", - "type": "table" - } - ], - "schemaVersion": 27, - "style": "dark", - "tags": [ - "gdev", - "panel-tests" - ], - "templating": { - "list": [] - }, - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ] - }, - "timezone": "", - "title": "Panel Tests - React Table", - "uid": "U_bZIMRMk", - "version": 6 + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Panel Tests - React Table", + "uid": "U_bZIMRMk", + "version": 33, + "weekStart": "" } \ No newline at end of file diff --git a/docs/sources/developers/kinds/composable/tablepanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/tablepanelcfg/schema-reference.md index 33e565a7039..f012f709d25 100644 --- a/docs/sources/developers/kinds/composable/tablepanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/tablepanelcfg/schema-reference.md @@ -23,6 +23,7 @@ title: TablePanelCfg kind |-----------------|---------------------------------------------------|----------|--------------------------------------------------------------------------------------| | `frameIndex` | number | **Yes** | Represents the index of the selected frame Default: `0`. | | `showHeader` | boolean | **Yes** | Controls whether the panel should show the header Default: `true`. | +| `cellHeight` | string | No | Height of a table cell
Possible values are: `sm`, `md`, `lg`. | | `footer` | [object](#footer) | No | Controls footer options Default: `map[countRows:false reducer:[] show:false]`. | | `showRowNums` | boolean | No | Controls whether the columns should be numbered Default: `false`. | | `showTypeIcons` | boolean | No | Controls whether the header should show icons for the column types Default: `false`. | diff --git a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md index 8fa2a5dd58e..0099b631dfb 100644 --- a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md +++ b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md @@ -732,3 +732,9 @@ Here is the result after adding a Limit transformation with a value of '3': | 2020-07-07 11:34:20 | Temperature | 25 | | 2020-07-07 11:34:20 | Humidity | 22 | | 2020-07-07 10:32:20 | Humidity | 29 | + +### Time series to table transform + +> **Note:** This transformation is available in Grafana 9.5+ as an opt-in beta feature. Modify Grafana [configuration file]({{< relref "../../../setup-grafana/configure-grafana/#configuration-file-location" >}}) to enable the `timeSeriesTable` [feature toggle]({{< relref "../../../setup-grafana/configure-grafana/#feature_toggles" >}}) to use it. + +Use this transformation to convert time series result into a table, converting time series data frame into a "Trend" field. "Trend" field can then be rendered using [sparkline cell type]({{< relref "../../visualizations/table/#sparkline" >}}), producing an inline sparkline for each table row. If there are multiple time series queries, each will result in a separate table data frame. These can be joined using join or merge transforms to produce a single table with multiple sparklines per row. diff --git a/docs/sources/panels-visualizations/visualizations/table/index.md b/docs/sources/panels-visualizations/visualizations/table/index.md index 8e3e6b13d6c..089d7d3ffe4 100644 --- a/docs/sources/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/panels-visualizations/visualizations/table/index.md @@ -122,6 +122,14 @@ If you have a field value that is an image URL or a base64 encoded image you can {{< figure src="/static/img/docs/v73/table_hover.gif" max-width="900px" caption="Table hover" >}} +### Sparkline + +> **Note:** This cell type is available in Grafana 9.5+ as an opt-in beta feature. Modify Grafana [configuration file]({{< relref "../../../setup-grafana/configure-grafana/#configuration-file-location" >}}) to enable the `timeSeriesTable` [feature toggle]({{< relref "../../../setup-grafana/configure-grafana/#feature_toggles" >}}) to use it. + +Shows value rendered as a sparkline. Requires [time series to table]({{< relref "../../query-transform-data/transform-data/#time-series-to-table-transform" >}}) data transform. + +{{< figure src="/static/img/docs/tables/sparkline.png" max-width="500px" caption="Sparkline" class="docs-image--no-shadow" >}} + ## Cell value inspect Enables value inspection from table cell. The raw value is presented in a modal window. diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 75a0e5119f9..deb9903ffa8 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -94,6 +94,7 @@ Alpha features might be changed or removed without prior notice. | `drawerDataSourcePicker` | Changes the user experience for data source selection to a drawer. | | `traceqlSearch` | Enables the 'TraceQL Search' tab for the Tempo datasource which provides a UI to generate TraceQL queries | | `prometheusMetricEncyclopedia` | Replaces the Prometheus query builder metric select option with a paginated and filterable component | +| `timeSeriesTable` | Enable time series table transformer & sparkline cell type | ## Development feature toggles diff --git a/packages/grafana-data/src/dataframe/index.ts b/packages/grafana-data/src/dataframe/index.ts index 3267885b2c0..3213eb70dab 100644 --- a/packages/grafana-data/src/dataframe/index.ts +++ b/packages/grafana-data/src/dataframe/index.ts @@ -7,4 +7,4 @@ export * from './dimensions'; export * from './ArrayDataFrame'; export * from './DataFrameJSON'; export * from './frameComparisons'; -export { anySeriesWithTimeField } from './utils'; +export { anySeriesWithTimeField, isTimeSeriesFrame, isTimeSeriesFrames } from './utils'; diff --git a/packages/grafana-data/src/dataframe/utils.ts b/packages/grafana-data/src/dataframe/utils.ts index 0869d2ec135..774c6cd4b6d 100644 --- a/packages/grafana-data/src/dataframe/utils.ts +++ b/packages/grafana-data/src/dataframe/utils.ts @@ -2,15 +2,15 @@ import { DataFrame, FieldType } from '../types/dataFrame'; import { getTimeField } from './processDataFrame'; -export function isTimeSerie(frame: DataFrame) { +export function isTimeSeriesFrame(frame: DataFrame) { if (frame.fields.length > 2) { return false; } return Boolean(frame.fields.find((field) => field.type === FieldType.time)); } -export function isTimeSeries(data: DataFrame[]) { - return !data.find((frame) => !isTimeSerie(frame)); +export function isTimeSeriesFrames(data: DataFrame[]) { + return !data.find((frame) => !isTimeSeriesFrame(frame)); } /** diff --git a/packages/grafana-data/src/field/index.ts b/packages/grafana-data/src/field/index.ts index 8ae807d42bc..9462ca10687 100644 --- a/packages/grafana-data/src/field/index.ts +++ b/packages/grafana-data/src/field/index.ts @@ -15,4 +15,4 @@ export { sortThresholds, getActiveThreshold } from './thresholds'; export { applyFieldOverrides, validateFieldConfig, applyRawFieldOverrides, useFieldOverrides } from './fieldOverrides'; export { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy'; export { getFieldDisplayName, getFrameDisplayName } from './fieldState'; -export { getScaleCalculator, getFieldConfigWithMinMax } from './scale'; +export { getScaleCalculator, getFieldConfigWithMinMax, getMinMaxAndDelta } from './scale'; diff --git a/packages/grafana-data/src/transformations/transformers/ids.ts b/packages/grafana-data/src/transformations/transformers/ids.ts index 968958d9668..bce099ea1ca 100644 --- a/packages/grafana-data/src/transformations/transformers/ids.ts +++ b/packages/grafana-data/src/transformations/transformers/ids.ts @@ -36,4 +36,5 @@ export enum DataTransformerID { groupingToMatrix = 'groupingToMatrix', limit = 'limit', partitionByValues = 'partitionByValues', + timeSeriesTable = 'timeSeriesTable', } diff --git a/packages/grafana-data/src/transformations/transformers/seriesToRows.ts b/packages/grafana-data/src/transformations/transformers/seriesToRows.ts index 8121784b476..3e32de48faa 100644 --- a/packages/grafana-data/src/transformations/transformers/seriesToRows.ts +++ b/packages/grafana-data/src/transformations/transformers/seriesToRows.ts @@ -2,7 +2,7 @@ import { omit } from 'lodash'; import { map } from 'rxjs/operators'; import { MutableDataFrame, sortDataFrame } from '../../dataframe'; -import { isTimeSeries } from '../../dataframe/utils'; +import { isTimeSeriesFrames } from '../../dataframe/utils'; import { getFrameDisplayName } from '../../field/fieldState'; import { Field, @@ -30,7 +30,7 @@ export const seriesToRowsTransformer: DataTransformerInfo { pxAlign: false, scaleKey, theme, + colorMode, + thresholds: config.thresholds, drawStyle: customConfig.drawStyle!, lineColor: customConfig.lineColor ?? seriesColor, lineWidth: customConfig.lineWidth, @@ -188,7 +191,9 @@ export class Sparkline extends PureComponent { showPoints: pointsMode, pointSize: customConfig.pointSize, fillOpacity: customConfig.fillOpacity, - fillColor: customConfig.fillColor ?? seriesColor, + fillColor: customConfig.fillColor, + lineStyle: customConfig.lineStyle, + gradientMode: customConfig.gradientMode, }); } diff --git a/packages/grafana-ui/src/components/Table/FooterRow.tsx b/packages/grafana-ui/src/components/Table/FooterRow.tsx index 8f598b680b1..83c8c0e6cd5 100644 --- a/packages/grafana-ui/src/components/Table/FooterRow.tsx +++ b/packages/grafana-ui/src/components/Table/FooterRow.tsx @@ -16,7 +16,7 @@ export interface FooterRowProps { tableStyles: TableStyles; } -export const FooterRow = (props: FooterRowProps) => { +export function FooterRow(props: FooterRowProps) { const { totalColumnsWidth, footerGroups, isPaginationVisible, tableStyles } = props; const e2eSelectorsTable = selectors.components.Panels.Visualization.Table; @@ -38,7 +38,7 @@ export const FooterRow = (props: FooterRowProps) => { })}
); -}; +} function renderFooterCell(column: ColumnInstance, tableStyles: TableStyles) { const footerProps = column.getHeaderProps(); diff --git a/packages/grafana-ui/src/components/Table/SparklineCell.tsx b/packages/grafana-ui/src/components/Table/SparklineCell.tsx new file mode 100644 index 00000000000..7da625fe5a7 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/SparklineCell.tsx @@ -0,0 +1,114 @@ +import { isArray } from 'lodash'; +import React, { FC } from 'react'; + +import { + ArrayVector, + FieldType, + FieldConfig, + getMinMaxAndDelta, + FieldSparkline, + isDataFrame, + Field, +} from '@grafana/data'; +import { + BarAlignment, + GraphDrawStyle, + GraphFieldConfig, + GraphGradientMode, + LineInterpolation, + TableSparklineCellOptions, + TableCellDisplayMode, + VisibilityMode, +} from '@grafana/schema'; + +import { Sparkline } from '../Sparkline/Sparkline'; + +import { TableCellProps } from './types'; +import { getCellOptions } from './utils'; + +export const defaultSparklineCellConfig: GraphFieldConfig = { + drawStyle: GraphDrawStyle.Line, + lineInterpolation: LineInterpolation.Smooth, + lineWidth: 1, + fillOpacity: 17, + gradientMode: GraphGradientMode.Hue, + pointSize: 2, + barAlignment: BarAlignment.Center, + showPoints: VisibilityMode.Never, +}; + +export const SparklineCell: FC = (props) => { + const { field, innerWidth, tableStyles, cell, cellProps } = props; + + const sparkline = getSparkline(cell.value); + + if (!sparkline) { + return ( +
+ no data +
+ ); + } + + const range = getMinMaxAndDelta(sparkline.y); + sparkline.y.config.min = range.min; + sparkline.y.config.max = range.max; + sparkline.y.state = { range }; + + const cellOptions = getTableSparklineCellOptions(field); + + const config: FieldConfig = { + color: field.config.color, + custom: { + ...defaultSparklineCellConfig, + ...cellOptions, + }, + }; + + return ( +
+ +
+ ); +}; + +function getSparkline(value: unknown): FieldSparkline | undefined { + if (isArray(value)) { + return { + y: { + name: 'test', + type: FieldType.number, + values: new ArrayVector(value), + config: {}, + }, + }; + } + + if (isDataFrame(value)) { + const timeField = value.fields.find((x) => x.type === FieldType.time); + const numberField = value.fields.find((x) => x.type === FieldType.number); + + if (timeField && numberField) { + return { x: timeField, y: numberField }; + } + } + + return; +} + +function getTableSparklineCellOptions(field: Field): TableSparklineCellOptions { + let options = getCellOptions(field); + if (options.type === TableCellDisplayMode.Auto) { + options = { ...options, type: TableCellDisplayMode.Sparkline }; + } + if (options.type === TableCellDisplayMode.Sparkline) { + return options; + } + throw new Error(`Excpected options type ${TableCellDisplayMode.Sparkline} but got ${options.type}`); +} diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 364d15dd283..90d2633025f 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -13,8 +13,9 @@ import { import { VariableSizeList } from 'react-window'; import { DataFrame, Field, ReducerID } from '@grafana/data'; +import { TableCellHeight } from '@grafana/schema'; -import { useStyles2, useTheme2 } from '../../themes'; +import { useTheme2 } from '../../themes'; import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar'; import { Pagination } from '../Pagination/Pagination'; @@ -23,7 +24,7 @@ import { HeaderRow } from './HeaderRow'; import { TableCell } from './TableCell'; import { useFixScrollbarContainer, useResetVariableListSizeCache } from './hooks'; import { getInitialState, useTableStateReducer } from './reducer'; -import { getTableStyles } from './styles'; +import { useTableStyles } from './styles'; import { FooterItem, GrafanaTableState, Props } from './types'; import { getColumns, @@ -56,13 +57,14 @@ export const Table = memo((props: Props) => { showTypeIcons, footerValues, enablePagination, + cellHeight = TableCellHeight.Md, } = props; const listRef = useRef(null); const tableDivRef = useRef(null); const variableSizeListScrollbarRef = useRef(null); - const tableStyles = useStyles2(getTableStyles); const theme = useTheme2(); + const tableStyles = useTableStyles(theme, cellHeight); const headerHeight = noHeader ? 0 : tableStyles.rowHeight; const [footerItems, setFooterItems] = useState(footerValues); @@ -385,6 +387,8 @@ export const Table = memo((props: Props) => {
{ +export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCellHeight) { const borderColor = theme.colors.border.weak; const resizerColor = theme.colors.primary.border; const cellPadding = 6; - const lineHeight = theme.typography.body.lineHeight; - const bodyFontSize = 14; - const cellHeight = cellPadding * 2 + bodyFontSize * lineHeight; + const cellHeight = getCellHeight(theme, cellHeightOption, cellPadding); const rowHeight = cellHeight + 2; + const headerHeight = 28; const rowHoverBg = theme.colors.emphasize(theme.colors.background.primary, 0.03); const buildCellContainerStyle = (color?: string, background?: string, overflowOnHover?: boolean) => { @@ -95,7 +95,7 @@ export const getTableStyles = (theme: GrafanaTheme2) => { cellHeight, buildCellContainerStyle, cellPadding, - cellHeightInner: bodyFontSize * lineHeight, + cellHeightInner: cellHeight - cellPadding * 2, rowHeight, table: css` height: 100%; @@ -106,14 +106,14 @@ export const getTableStyles = (theme: GrafanaTheme2) => { `, thead: css` label: thead; - height: ${rowHeight}px; + height: ${headerHeight}px; overflow-y: auto; overflow-x: hidden; position: relative; `, tfoot: css` label: tfoot; - height: ${rowHeight}px; + height: ${headerHeight}px; border-top: 1px solid ${borderColor}; overflow-y: auto; overflow-x: hidden; @@ -124,10 +124,12 @@ export const getTableStyles = (theme: GrafanaTheme2) => { border-bottom: 1px solid ${borderColor}; `, headerCell: css` - padding: ${cellPadding}px; + height: 100%; + padding: 0 ${cellPadding}px; overflow: hidden; white-space: nowrap; display: flex; + align-items: center; font-weight: ${theme.typography.fontWeightMedium}; &:last-child { @@ -285,6 +287,21 @@ export const getTableStyles = (theme: GrafanaTheme2) => { cursor: pointer; `, }; -}; +} -export type TableStyles = ReturnType; +export type TableStyles = ReturnType; + +function getCellHeight(theme: GrafanaTheme2, cellHeightOption: TableCellHeight, cellPadding: number) { + const bodyFontSize = theme.typography.fontSize; + const lineHeight = theme.typography.body.lineHeight; + + switch (cellHeightOption) { + case 'md': + return 42; + case 'lg': + return 48; + case 'sm': + default: + return cellPadding * 2 + bodyFontSize * lineHeight; + } +} diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index ba7f9ad3d6d..7ee4f584753 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -3,6 +3,7 @@ import { FC } from 'react'; import { CellProps, Column, Row, TableState, UseExpandedRowProps } from 'react-table'; import { DataFrame, Field, KeyValue, SelectableValue } from '@grafana/data'; +import { TableCellHeight } from '@grafana/schema'; import { TableStyles } from './styles'; @@ -84,6 +85,7 @@ export interface Props { footerOptions?: TableFooterCalc; footerValues?: FooterItem[]; enablePagination?: boolean; + cellHeight?: TableCellHeight; /** @alpha */ subData?: DataFrame[]; } diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts index d4ed918755b..051da3e546f 100644 --- a/packages/grafana-ui/src/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -15,6 +15,8 @@ import { reduceField, GrafanaTheme2, ArrayVector, + isDataFrame, + isTimeSeriesFrame, } from '@grafana/data'; import { BarGaugeDisplayMode, @@ -30,6 +32,7 @@ import { GeoCell } from './GeoCell'; import { ImageCell } from './ImageCell'; import { JSONViewCell } from './JSONViewCell'; import { RowExpander } from './RowExpander'; +import { SparklineCell } from './SparklineCell'; import { CellComponent, TableCellDisplayMode, @@ -190,6 +193,8 @@ export function getCellComponent(displayMode: TableCellDisplayMode, field: Field return ImageCell; case TableCellDisplayMode.Gauge: return BarGaugeCell; + case TableCellDisplayMode.Sparkline: + return SparklineCell; case TableCellDisplayMode.JSONView: return JSONViewCell; } @@ -198,10 +203,20 @@ export function getCellComponent(displayMode: TableCellDisplayMode, field: Field return GeoCell; } + if (field.type === FieldType.frame) { + const firstValue = field.values.get(0); + if (isDataFrame(firstValue) && isTimeSeriesFrame(firstValue)) { + return SparklineCell; + } + + return JSONViewCell; + } + // Default or Auto if (field.type === FieldType.other) { return JSONViewCell; } + return DefaultCell; } diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index 89774222968..52eb0bd3489 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -21,4 +21,5 @@ const ( grafanaAlertingSquad codeowner = "@grafana/alerting-squad" hostedGrafanaTeam codeowner = "@grafana/hosted-grafana-team" awsPluginsSquad codeowner = "@grafana/aws-plugins" + appO11ySquad codeowner = "@grafana/app-o11y" ) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index cacfb8a7864..74b8985acd7 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -438,5 +438,12 @@ var ( FrontendOnly: true, Owner: grafanaObservabilityMetricsSquad, }, + { + Name: "timeSeriesTable", + Description: "Enable time series table transformer & sparkline cell type", + State: FeatureStateAlpha, + FrontendOnly: true, + Owner: appO11ySquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index b1541eb9369..d4aea01c0e1 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -270,4 +270,8 @@ const ( // FlagPrometheusMetricEncyclopedia // Replaces the Prometheus query builder metric select option with a paginated and filterable component FlagPrometheusMetricEncyclopedia = "prometheusMetricEncyclopedia" + + // FlagTimeSeriesTable + // Enable time series table transformer & sparkline cell type + FlagTimeSeriesTable = "timeSeriesTable" ) diff --git a/public/app/features/alerting/unified/components/expressions/Expression.tsx b/public/app/features/alerting/unified/components/expressions/Expression.tsx index b2767a1ea3d..7504a8e5e70 100644 --- a/public/app/features/alerting/unified/components/expressions/Expression.tsx +++ b/public/app/features/alerting/unified/components/expressions/Expression.tsx @@ -2,8 +2,7 @@ import { css, cx } from '@emotion/css'; import { capitalize, uniqueId } from 'lodash'; import React, { FC, useCallback, useState } from 'react'; -import { DataFrame, dateTimeFormat, GrafanaTheme2, LoadingState, PanelData } from '@grafana/data'; -import { isTimeSeries } from '@grafana/data/src/dataframe/utils'; +import { DataFrame, dateTimeFormat, GrafanaTheme2, LoadingState, PanelData, isTimeSeriesFrames } from '@grafana/data'; import { Stack } from '@grafana/experimental'; import { AutoSizeInput, clearButtonStyles, Icon, IconButton, Select, useStyles2 } from '@grafana/ui'; import { ClassicConditions } from 'app/features/expressions/components/ClassicConditions'; @@ -138,7 +137,7 @@ export const ExpressionResult: FC = ({ series, isAlertCon // sometimes we receive results where every value is just "null" when noData occurs const emptyResults = isEmptySeries(series); - const isTimeSeriesResults = !emptyResults && isTimeSeries(series); + const isTimeSeriesResults = !emptyResults && isTimeSeriesFrames(series); return (
diff --git a/public/app/features/alerting/unified/components/rule-editor/util.ts b/public/app/features/alerting/unified/components/rule-editor/util.ts index a16eee6e922..89dfe3e2717 100644 --- a/public/app/features/alerting/unified/components/rule-editor/util.ts +++ b/public/app/features/alerting/unified/components/rule-editor/util.ts @@ -1,7 +1,6 @@ import { ValidateResult } from 'react-hook-form'; -import { DataFrame, ThresholdsConfig, ThresholdsMode } from '@grafana/data'; -import { isTimeSeries } from '@grafana/data/src/dataframe/utils'; +import { DataFrame, ThresholdsConfig, ThresholdsMode, isTimeSeriesFrames } from '@grafana/data'; import { GraphTresholdsStyleMode } from '@grafana/schema'; import { config } from 'app/core/config'; import { EvalFunction } from 'app/features/alerting/state/alertDef'; @@ -98,7 +97,7 @@ export function errorFromSeries(series: DataFrame[]): Error | undefined { return; } - const isTimeSeriesResults = isTimeSeries(series); + const isTimeSeriesResults = isTimeSeriesFrames(series); let error; if (isTimeSeriesResults) { diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index 7b37837b294..c3d5d96882c 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -7,9 +7,10 @@ import InfiniteLoader from 'react-window-infinite-loader'; import { Observable } from 'rxjs'; import { Field, GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; +import { TableCellHeight } from '@grafana/schema'; +import { useStyles2, useTheme2 } from '@grafana/ui'; import { TableCell } from '@grafana/ui/src/components/Table/TableCell'; -import { getTableStyles } from '@grafana/ui/src/components/Table/styles'; +import { useTableStyles } from '@grafana/ui/src/components/Table/styles'; import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection'; import { QueryResponse } from '../../service'; @@ -51,7 +52,7 @@ export const SearchResultsTable = React.memo( }: SearchResultsProps) => { const styles = useStyles2(getStyles); const columnStyles = useStyles2(getColumnStyles); - const tableStyles = useStyles2(getTableStyles); + const tableStyles = useTableStyles(useTheme2(), TableCellHeight.Md); const infiniteLoaderRef = useRef(null); const [listEl, setListEl] = useState(null); const highlightIndex = useSearchKeyboardNavigation(keyboardEvents, 0, response); diff --git a/public/app/features/transformers/standardTransformers.ts b/public/app/features/transformers/standardTransformers.ts index c047c59ab14..8716a48a20a 100644 --- a/public/app/features/transformers/standardTransformers.ts +++ b/public/app/features/transformers/standardTransformers.ts @@ -1,4 +1,5 @@ import { TransformerRegistryItem } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { filterByValueTransformRegistryItem } from './FilterByValueTransformer/FilterByValueTransformerEditor'; import { heatmapTransformRegistryItem } from './calculateHeatmap/HeatmapTransformerEditor'; @@ -27,6 +28,7 @@ import { partitionByValuesTransformRegistryItem } from './partitionByValues/Part import { prepareTimeseriesTransformerRegistryItem } from './prepareTimeSeries/PrepareTimeSeriesEditor'; import { rowsToFieldsTransformRegistryItem } from './rowsToFields/RowsToFieldsTransformerEditor'; import { spatialTransformRegistryItem } from './spatial/SpatialTransformerEditor'; +import { timeSeriesTableTransformRegistryItem } from './timeSeriesTable/TimeSeriesTableTransformEditor'; export const getStandardTransformers = (): Array> => { return [ @@ -57,5 +59,6 @@ export const getStandardTransformers = (): Array> = limitTransformRegistryItem, joinByLabelsTransformRegistryItem, partitionByValuesTransformRegistryItem, + ...(config.featureToggles.timeSeriesTable ? [timeSeriesTableTransformRegistryItem] : []), ]; }; diff --git a/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx b/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx new file mode 100644 index 00000000000..eb01f21de1b --- /dev/null +++ b/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import { PluginState, TransformerRegistryItem, TransformerUIProps } from '@grafana/data'; + +import { timeSeriesTableTransformer, TimeSeriesTableTransformerOptions } from './timeSeriesTableTransformer'; + +export interface Props extends TransformerUIProps<{}> {} + +export function TimeSeriesTableTransformEditor({ input, options, onChange }: Props) { + if (input.length === 0) { + return null; + } + + return
; +} + +export const timeSeriesTableTransformRegistryItem: TransformerRegistryItem = { + id: timeSeriesTableTransformer.id, + editor: TimeSeriesTableTransformEditor, + transformation: timeSeriesTableTransformer, + name: timeSeriesTableTransformer.name, + description: timeSeriesTableTransformer.description, + state: PluginState.beta, + help: ``, +}; diff --git a/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.test.ts b/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.test.ts new file mode 100644 index 00000000000..952fcdf0d9b --- /dev/null +++ b/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.test.ts @@ -0,0 +1,104 @@ +import { toDataFrame, FieldType, Labels, DataFrame, Field } from '@grafana/data'; + +import { timeSeriesToTableTransform } from './timeSeriesTableTransformer'; + +describe('timeSeriesTableTransformer', () => { + it('Will transform a single query', () => { + const series = [ + getTimeSeries('A', { instance: 'A', pod: 'B' }), + getTimeSeries('A', { instance: 'A', pod: 'C' }), + getTimeSeries('A', { instance: 'A', pod: 'D' }), + ]; + + const results = timeSeriesToTableTransform({}, series); + expect(results).toHaveLength(1); + const result = results[0]; + expect(result.refId).toBe('A'); + expect(result.fields).toHaveLength(3); + expect(result.fields[0].values.toArray()).toEqual(['A', 'A', 'A']); + expect(result.fields[1].values.toArray()).toEqual(['B', 'C', 'D']); + assertDataFrameField(result.fields[2], series); + }); + + it('Will pass through non time series frames', () => { + const series = [ + getTable('B', ['foo', 'bar']), + getTimeSeries('A', { instance: 'A', pod: 'B' }), + getTimeSeries('A', { instance: 'A', pod: 'C' }), + getTable('C', ['bar', 'baz', 'bad']), + ]; + + const results = timeSeriesToTableTransform({}, series); + expect(results).toHaveLength(3); + expect(results[0]).toEqual(series[0]); + expect(results[1].refId).toBe('A'); + expect(results[1].fields).toHaveLength(3); + expect(results[1].fields[0].values.toArray()).toEqual(['A', 'A']); + expect(results[1].fields[1].values.toArray()).toEqual(['B', 'C']); + expect(results[2]).toEqual(series[3]); + }); + + it('Will group by refId', () => { + const series = [ + getTimeSeries('A', { instance: 'A', pod: 'B' }), + getTimeSeries('A', { instance: 'A', pod: 'C' }), + getTimeSeries('A', { instance: 'A', pod: 'D' }), + getTimeSeries('B', { instance: 'B', pod: 'F', cluster: 'A' }), + getTimeSeries('B', { instance: 'B', pod: 'G', cluster: 'B' }), + ]; + + const results = timeSeriesToTableTransform({}, series); + expect(results).toHaveLength(2); + expect(results[0].refId).toBe('A'); + expect(results[0].fields).toHaveLength(3); + expect(results[0].fields[0].values.toArray()).toEqual(['A', 'A', 'A']); + expect(results[0].fields[1].values.toArray()).toEqual(['B', 'C', 'D']); + assertDataFrameField(results[0].fields[2], series.slice(0, 3)); + expect(results[1].refId).toBe('B'); + expect(results[1].fields).toHaveLength(4); + expect(results[1].fields[0].values.toArray()).toEqual(['B', 'B']); + expect(results[1].fields[1].values.toArray()).toEqual(['F', 'G']); + expect(results[1].fields[2].values.toArray()).toEqual(['A', 'B']); + assertDataFrameField(results[1].fields[3], series.slice(3, 5)); + }); +}); + +function assertFieldsEqual(field1: Field, field2: Field) { + expect(field1.type).toEqual(field2.type); + expect(field1.name).toEqual(field2.name); + expect(field1.values.toArray()).toEqual(field2.values.toArray()); + expect(field1.labels ?? {}).toEqual(field2.labels ?? {}); +} + +function assertDataFrameField(field: Field, matchesFrames: DataFrame[]) { + const frames: DataFrame[] = field.values.toArray(); + expect(frames).toHaveLength(matchesFrames.length); + frames.forEach((frame, idx) => { + const matchingFrame = matchesFrames[idx]; + expect(frame.fields).toHaveLength(matchingFrame.fields.length); + frame.fields.forEach((field, fidx) => assertFieldsEqual(field, matchingFrame.fields[fidx])); + }); +} + +function getTimeSeries(refId: string, labels: Labels) { + return toDataFrame({ + refId, + fields: [ + { name: 'Time', type: FieldType.time, values: [10] }, + { + name: 'Value', + type: FieldType.number, + values: [10], + labels, + }, + ], + }); +} + +function getTable(refId: string, fields: string[]) { + return toDataFrame({ + refId, + fields: fields.map((f) => ({ name: f, type: FieldType.string, values: ['value'] })), + labels: {}, + }); +} diff --git a/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts b/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts new file mode 100644 index 00000000000..d3e6b68a12c --- /dev/null +++ b/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts @@ -0,0 +1,127 @@ +import { map } from 'rxjs/operators'; + +import { + ArrayVector, + DataFrame, + DataTransformerID, + DataTransformerInfo, + Field, + FieldType, + MutableDataFrame, + isTimeSeriesFrame, +} from '@grafana/data'; + +export interface TimeSeriesTableTransformerOptions {} + +export const timeSeriesTableTransformer: DataTransformerInfo = { + id: DataTransformerID.timeSeriesTable, + name: 'Time series to table transform', + description: 'Time series to table rows', + defaultOptions: {}, + + operator: (options) => (source) => + source.pipe( + map((data) => { + return timeSeriesToTableTransform(options, data); + }) + ), +}; + +/** + * Converts time series frames to table frames for use with sparkline chart type. + * + * @remarks + * For each refId (queryName) convert all time series frames into a single table frame, adding each series + * as values of a "Trend" frame field. This allows "Trend" to be rendered as area chart type. + * Any non time series frames are returned as is. + * + * @param options - Transform options, currently not used + * @param data - Array of data frames to transform + * @returns Array of transformed data frames + * + * @alpha + */ +export function timeSeriesToTableTransform(options: TimeSeriesTableTransformerOptions, data: DataFrame[]): DataFrame[] { + // initialize fields from labels for each refId + const refId2LabelFields = getLabelFields(data); + + const refId2frameField: Record> = {}; + + const result: DataFrame[] = []; + + for (const frame of data) { + if (!isTimeSeriesFrame(frame)) { + result.push(frame); + continue; + } + + const refId = frame.refId ?? ''; + + const labelFields = refId2LabelFields[refId] ?? {}; + // initialize a new frame for this refId with fields per label and a Trend frame field, if it doesn't exist yet + let frameField = refId2frameField[refId]; + if (!frameField) { + frameField = { + name: 'Trend' + (refId && Object.keys(refId2LabelFields).length > 1 ? ` #${refId}` : ''), + type: FieldType.frame, + config: {}, + values: new ArrayVector(), + }; + refId2frameField[refId] = frameField; + const table = new MutableDataFrame(); + for (const label of Object.values(labelFields)) { + table.addField(label); + } + table.addField(frameField); + table.refId = refId; + result.push(table); + } + + // add values to each label based field of this frame + const labels = frame.fields[1].labels; + for (const labelKey of Object.keys(labelFields)) { + const labelValue = labels?.[labelKey] ?? null; + labelFields[labelKey].values.add(labelValue); + } + + frameField.values.add(frame); + } + return result; +} + +// For each refId, initialize a field for each label name +function getLabelFields(frames: DataFrame[]): Record>> { + // refId -> label name -> field + const labelFields: Record>> = {}; + + for (const frame of frames) { + if (!isTimeSeriesFrame(frame)) { + continue; + } + + const refId = frame.refId ?? ''; + + if (!labelFields[refId]) { + labelFields[refId] = {}; + } + + for (const field of frame.fields) { + if (!field.labels) { + continue; + } + + for (const labelName of Object.keys(field.labels)) { + if (!labelFields[refId][labelName]) { + labelFields[refId][labelName] = { + name: labelName, + type: FieldType.string, + config: {}, + values: new ArrayVector(), + }; + } + } + } + } + + return labelFields; +} diff --git a/public/app/plugins/panel/table/TableCellOptionEditor.tsx b/public/app/plugins/panel/table/TableCellOptionEditor.tsx index c1bf6854791..ba23e886ddd 100644 --- a/public/app/plugins/panel/table/TableCellOptionEditor.tsx +++ b/public/app/plugins/panel/table/TableCellOptionEditor.tsx @@ -2,11 +2,13 @@ import { merge } from 'lodash'; import React, { useState } from 'react'; import { SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { TableCellOptions } from '@grafana/schema'; import { Field, Select, TableCellDisplayMode } from '@grafana/ui'; import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor'; import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor'; +import { SparklineCellOptionsEditor } from './cells/SparklineCellOptionsEditor'; // The props that any cell type editor are expected // to handle. In this case the generic type should @@ -64,12 +66,21 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => { {cellType === TableCellDisplayMode.ColorBackground && ( )} + {cellType === TableCellDisplayMode.Sparkline && ( + + )} ); }; +const SparklineDisplayModeOption: SelectableValue = { + value: { type: TableCellDisplayMode.Sparkline }, + label: 'Sparkline', +}; + const cellDisplayModeOptions: Array> = [ { value: { type: TableCellDisplayMode.Auto }, label: 'Auto' }, + ...(config.featureToggles.timeSeriesTable ? [SparklineDisplayModeOption] : []), { value: { type: TableCellDisplayMode.ColorText }, label: 'Colored text' }, { value: { type: TableCellDisplayMode.ColorBackground }, label: 'Colored background' }, { value: { type: TableCellDisplayMode.Gauge }, label: 'Gauge' }, diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index 3ae842d3d59..485024b55d9 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -56,6 +56,7 @@ export function TablePanel(props: Props) { footerOptions={options.footer} enablePagination={options.footer?.enablePagination} subData={subData} + cellHeight={options.cellHeight} /> ); diff --git a/public/app/plugins/panel/table/cells/SparklineCellOptionsEditor.tsx b/public/app/plugins/panel/table/cells/SparklineCellOptionsEditor.tsx new file mode 100644 index 00000000000..2e7ade9d591 --- /dev/null +++ b/public/app/plugins/panel/table/cells/SparklineCellOptionsEditor.tsx @@ -0,0 +1,79 @@ +import { css } from '@emotion/css'; +import React, { useMemo } from 'react'; + +import { createFieldConfigRegistry } from '@grafana/data'; +import { GraphFieldConfig, TableSparklineCellOptions } from '@grafana/schema'; +import { VerticalGroup, Field, useStyles2 } from '@grafana/ui'; +import { defaultSparklineCellConfig } from '@grafana/ui/src/components/Table/SparklineCell'; + +import { getGraphFieldConfig } from '../../timeseries/config'; +import { TableCellEditorProps } from '../TableCellOptionEditor'; + +type OptionKey = keyof TableSparklineCellOptions; + +const optionIds: Array = [ + 'drawStyle', + 'lineInterpolation', + 'barAlignment', + 'lineWidth', + 'fillOpacity', + 'gradientMode', + 'lineStyle', + 'spanNulls', + 'showPoints', + 'pointSize', +]; + +export const SparklineCellOptionsEditor = (props: TableCellEditorProps) => { + const { cellOptions, onChange } = props; + + const registry = useMemo(() => { + const config = getGraphFieldConfig(defaultSparklineCellConfig); + return createFieldConfigRegistry(config, 'ChartCell'); + }, []); + + const style = useStyles2(getStyles); + + const values = { ...defaultSparklineCellConfig, ...cellOptions }; + + return ( + + {registry.list(optionIds.map((id) => `custom.${id}`)).map((item) => { + if (item.showIf && !item.showIf(values)) { + return null; + } + const Editor = item.editor; + const path = item.path; + + return ( + + onChange({ ...cellOptions, [path]: val })} + value={(isOptionKey(path, values) ? values[path] : undefined) ?? item.defaultValue} + item={item} + context={{ data: [] }} + /> + + ); + })} + + ); +}; + +// jumping through hoops to avoid using "any" +function isOptionKey(key: string, options: TableSparklineCellOptions): key is OptionKey { + return key in options; +} + +const getStyles = () => ({ + field: css` + width: 100%; + + // @TODO don't show "scheme" option for custom gradient mode. + // it needs thresholds to work, which are not supported + // for area chart cell right now + [title='Use color scheme to define gradient'] { + display: none; + } + `, +}); diff --git a/public/app/plugins/panel/table/module.tsx b/public/app/plugins/panel/table/module.tsx index 9604f5c2df7..6a11e5ec321 100644 --- a/public/app/plugins/panel/table/module.tsx +++ b/public/app/plugins/panel/table/module.tsx @@ -7,7 +7,13 @@ import { standardEditorsRegistry, identityOverrideProcessor, } from '@grafana/data'; -import { TableFieldOptions, TableCellOptions, TableCellDisplayMode, defaultTableFieldOptions } from '@grafana/schema'; +import { + TableFieldOptions, + TableCellOptions, + TableCellDisplayMode, + defaultTableFieldOptions, + TableCellHeight, +} from '@grafana/schema'; import { PaginationEditor } from './PaginationEditor'; import { TableCellOptionEditor } from './TableCellOptionEditor'; @@ -108,6 +114,18 @@ export const plugin = new PanelPlugin(TablePane name: 'Show table header', defaultValue: defaultPanelOptions.showHeader, }) + .addRadio({ + path: 'cellHeight', + name: 'Cell height', + defaultValue: defaultPanelOptions.cellHeight, + settings: { + options: [ + { value: TableCellHeight.Sm, label: 'Small' }, + { value: TableCellHeight.Md, label: 'Medium' }, + { value: TableCellHeight.Lg, label: 'Large' }, + ], + }, + }) .addBooleanSwitch({ path: 'showRowNums', name: 'Show row numbers', diff --git a/public/app/plugins/panel/table/panelcfg.cue b/public/app/plugins/panel/table/panelcfg.cue index a574c08d6b2..563561e380d 100644 --- a/public/app/plugins/panel/table/panelcfg.cue +++ b/public/app/plugins/panel/table/panelcfg.cue @@ -45,6 +45,8 @@ composableKinds: PanelCfg: { // Represents the selected calculations reducer: [] } + // Controls the height of the rows + cellHeight?: ui.TableCellHeight | *"md" } @cuetsy(kind="interface") }, ] diff --git a/public/app/plugins/panel/table/panelcfg.gen.ts b/public/app/plugins/panel/table/panelcfg.gen.ts index c0533fcb87a..85d617bc4ba 100644 --- a/public/app/plugins/panel/table/panelcfg.gen.ts +++ b/public/app/plugins/panel/table/panelcfg.gen.ts @@ -13,6 +13,10 @@ import * as ui from '@grafana/schema'; export const PanelCfgModelVersion = Object.freeze([0, 0]); export interface PanelOptions { + /** + * Controls the height of the rows + */ + cellHeight?: ui.TableCellHeight; /** * Controls footer options */ @@ -40,6 +44,7 @@ export interface PanelOptions { } export const defaultPanelOptions: Partial = { + cellHeight: ui.TableCellHeight.Md, footer: { /** * Controls whether the footer should be shown From 79152969f31e107384847d12bc70944305c014a5 Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Fri, 10 Mar 2023 13:55:38 +0100 Subject: [PATCH 12/33] Extensions: Expose an enum for available placements (#64586) * feat: expose an enum type for extension placements * fix: export the enum properly --- packages/grafana-data/src/types/index.ts | 1 + .../src/types/pluginExtensions.ts | 4 ++++ .../dashboard/utils/getPanelMenu.test.ts | 21 ++++++++++++------- .../features/dashboard/utils/getPanelMenu.ts | 10 ++++++--- .../features/plugins/extensions/placements.ts | 3 --- 5 files changed, 25 insertions(+), 14 deletions(-) delete mode 100644 public/app/features/plugins/extensions/placements.ts diff --git a/packages/grafana-data/src/types/index.ts b/packages/grafana-data/src/types/index.ts index 0a3020aff99..67328bc5fd8 100644 --- a/packages/grafana-data/src/types/index.ts +++ b/packages/grafana-data/src/types/index.ts @@ -61,4 +61,5 @@ export { isPluginExtensionCommand, assertPluginExtensionCommand, PluginExtensionTypes, + PluginExtensionPlacements, } from './pluginExtensions'; diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index c2a4215dd6d..f366ed23a94 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -2,6 +2,10 @@ * These types are exposed when rendering extension points */ +export enum PluginExtensionPlacements { + DashboardPanelMenu = 'grafana/dashboard/panel/menu', +} + export enum PluginExtensionTypes { link = 'link', command = 'command', diff --git a/public/app/features/dashboard/utils/getPanelMenu.test.ts b/public/app/features/dashboard/utils/getPanelMenu.test.ts index ca637b459f1..c548185062c 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.test.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.test.ts @@ -1,4 +1,10 @@ -import { PanelMenuItem, PluginExtension, PluginExtensionLink, PluginExtensionTypes } from '@grafana/data'; +import { + PanelMenuItem, + PluginExtension, + PluginExtensionLink, + PluginExtensionTypes, + PluginExtensionPlacements, +} from '@grafana/data'; import { PluginExtensionPanelContext, PluginExtensionRegistryItem, @@ -7,7 +13,6 @@ import { import { LoadingState } from '@grafana/schema'; import config from 'app/core/config'; import * as actions from 'app/features/explore/state/main'; -import { GrafanaExtensions } from 'app/features/plugins/extensions/placements'; import { setStore } from 'app/store/store'; import { PanelModel } from '../state'; @@ -138,7 +143,7 @@ describe('getPanelMenu()', () => { describe('when extending panel menu from plugins', () => { it('should contain menu item from link extension', () => { setPluginsExtensionRegistry({ - [GrafanaExtensions.DashboardPanelMenu]: [ + [PluginExtensionPlacements.DashboardPanelMenu]: [ createRegistryItem({ type: PluginExtensionTypes.link, title: 'Declare incident', @@ -166,7 +171,7 @@ describe('getPanelMenu()', () => { it('should truncate menu item title to 25 chars', () => { setPluginsExtensionRegistry({ - [GrafanaExtensions.DashboardPanelMenu]: [ + [PluginExtensionPlacements.DashboardPanelMenu]: [ createRegistryItem({ type: PluginExtensionTypes.link, title: 'Declare incident when pressing this amazing menu item', @@ -202,7 +207,7 @@ describe('getPanelMenu()', () => { }); setPluginsExtensionRegistry({ - [GrafanaExtensions.DashboardPanelMenu]: [ + [PluginExtensionPlacements.DashboardPanelMenu]: [ createRegistryItem( { type: PluginExtensionTypes.link, @@ -233,7 +238,7 @@ describe('getPanelMenu()', () => { it('should hide menu item if configure function returns undefined', () => { setPluginsExtensionRegistry({ - [GrafanaExtensions.DashboardPanelMenu]: [ + [PluginExtensionPlacements.DashboardPanelMenu]: [ createRegistryItem( { type: PluginExtensionTypes.link, @@ -266,7 +271,7 @@ describe('getPanelMenu()', () => { const configure = jest.fn(); setPluginsExtensionRegistry({ - [GrafanaExtensions.DashboardPanelMenu]: [ + [PluginExtensionPlacements.DashboardPanelMenu]: [ createRegistryItem( { type: PluginExtensionTypes.link, @@ -348,7 +353,7 @@ describe('getPanelMenu()', () => { }; setPluginsExtensionRegistry({ - [GrafanaExtensions.DashboardPanelMenu]: [ + [PluginExtensionPlacements.DashboardPanelMenu]: [ createRegistryItem( { type: PluginExtensionTypes.link, diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index 339281c4c45..a4195ce3eb3 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -1,4 +1,9 @@ -import { isPluginExtensionCommand, isPluginExtensionLink, PanelMenuItem } from '@grafana/data'; +import { + isPluginExtensionCommand, + isPluginExtensionLink, + PanelMenuItem, + PluginExtensionPlacements, +} from '@grafana/data'; import { AngularComponent, getDataSourceSrv, @@ -26,7 +31,6 @@ import { } from 'app/features/dashboard/utils/panel'; import { InspectTab } from 'app/features/inspector/types'; import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard'; -import { GrafanaExtensions } from 'app/features/plugins/extensions/placements'; import { store } from 'app/store/store'; import { navigateToExplore } from '../../explore/state/main'; @@ -295,7 +299,7 @@ export function getPanelMenu( } const { extensions } = getPluginExtensions({ - placement: GrafanaExtensions.DashboardPanelMenu, + placement: PluginExtensionPlacements.DashboardPanelMenu, context: createExtensionContext(panel, dashboard), }); diff --git a/public/app/features/plugins/extensions/placements.ts b/public/app/features/plugins/extensions/placements.ts deleted file mode 100644 index 27af2a0bc78..00000000000 --- a/public/app/features/plugins/extensions/placements.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum GrafanaExtensions { - DashboardPanelMenu = 'grafana/dashboard/panel/menu', -} From 93b32eec4b435005cb881e9008131f60782b8ec0 Mon Sep 17 00:00:00 2001 From: Virginia Cepeda Date: Fri, 10 Mar 2023 09:56:01 -0300 Subject: [PATCH 13/33] Alerting: fix users call 403 by calling /user instead of /users/{id} (#64544) Fetch user data with calling /user endpoint This avoids a permission error we were getting by calling /users/{id} --- public/app/features/alerting/unified/Analytics.test.ts | 8 ++++---- public/app/features/alerting/unified/Analytics.ts | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/public/app/features/alerting/unified/Analytics.test.ts b/public/app/features/alerting/unified/Analytics.test.ts index f884c8d40eb..bea69a3ade4 100644 --- a/public/app/features/alerting/unified/Analytics.test.ts +++ b/public/app/features/alerting/unified/Analytics.test.ts @@ -18,10 +18,10 @@ describe('isNewUser', function () { getBackendSrv().get = jest.fn().mockResolvedValue(newUser); - const isNew = await isNewUser(1); + const isNew = await isNewUser(); expect(isNew).toBe(true); expect(getBackendSrv().get).toHaveBeenCalledTimes(1); - expect(getBackendSrv().get).toHaveBeenCalledWith('/api/users/1'); + expect(getBackendSrv().get).toHaveBeenCalledWith('/api/user'); }); it('should return false if the user has been created prior to the last two weeks', async () => { @@ -32,9 +32,9 @@ describe('isNewUser', function () { getBackendSrv().get = jest.fn().mockResolvedValue(oldUser); - const isNew = await isNewUser(2); + const isNew = await isNewUser(); expect(isNew).toBe(false); expect(getBackendSrv().get).toHaveBeenCalledTimes(1); - expect(getBackendSrv().get).toHaveBeenCalledWith('/api/users/2'); + expect(getBackendSrv().get).toHaveBeenCalledWith('/api/user'); }); }); diff --git a/public/app/features/alerting/unified/Analytics.ts b/public/app/features/alerting/unified/Analytics.ts index ca8481e5136..da31b83ebf1 100644 --- a/public/app/features/alerting/unified/Analytics.ts +++ b/public/app/features/alerting/unified/Analytics.ts @@ -45,9 +45,9 @@ export function withPerformanceLogging Promise }; } -export async function isNewUser(userId: number) { +export async function isNewUser() { try { - const { createdAt } = await getBackendSrv().get(`/api/users/${userId}`); + const { createdAt } = await getBackendSrv().get(`/api/user`); const limitDateForNewUser = dateTime().subtract(USER_CREATION_MIN_DAYS, 'days'); const userCreationDate = dateTime(createdAt); @@ -61,7 +61,7 @@ export async function isNewUser(userId: number) { } export const trackNewAlerRuleFormSaved = async (props: AlertRuleTrackingProps) => { - const isNew = await isNewUser(props.user_id); + const isNew = await isNewUser(); if (isNew) { return; } @@ -69,7 +69,7 @@ export const trackNewAlerRuleFormSaved = async (props: AlertRuleTrackingProps) = }; export const trackNewAlerRuleFormCancelled = async (props: AlertRuleTrackingProps) => { - const isNew = await isNewUser(props.user_id); + const isNew = await isNewUser(); if (isNew) { return; } @@ -77,7 +77,7 @@ export const trackNewAlerRuleFormCancelled = async (props: AlertRuleTrackingProp }; export const trackNewAlerRuleFormError = async (props: AlertRuleTrackingProps & { error: string }) => { - const isNew = await isNewUser(props.user_id); + const isNew = await isNewUser(); if (isNew) { return; } From eb507dca899a2f9e5f7fde5d60dfa619282d6601 Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Fri, 10 Mar 2023 13:57:29 +0100 Subject: [PATCH 14/33] Remotecache: rename setbytearray/getbytearray to set/get and remove codec (#64470) Signed-off-by: bergquist --- pkg/infra/remotecache/database_storage.go | 32 +---- .../remotecache/database_storage_test.go | 9 +- pkg/infra/remotecache/memcached_storage.go | 39 +----- pkg/infra/remotecache/redis_storage.go | 43 +------ pkg/infra/remotecache/remotecache.go | 114 +++++++----------- pkg/infra/remotecache/remotecache_test.go | 112 ++++++++++++----- pkg/middleware/middleware_test.go | 2 +- pkg/services/anonymous/anonimpl/impl.go | 2 +- pkg/services/auth/jwt/key_sets.go | 4 +- pkg/services/authn/clients/proxy.go | 8 +- pkg/services/authn/clients/proxy_test.go | 4 +- .../contexthandler/auth_proxy_test.go | 4 +- .../contexthandler/authproxy/authproxy.go | 6 +- .../authproxy/authproxy_test.go | 4 +- pkg/services/rendering/auth.go | 4 +- 15 files changed, 164 insertions(+), 223 deletions(-) diff --git a/pkg/infra/remotecache/database_storage.go b/pkg/infra/remotecache/database_storage.go index 2d2ed1d34ca..af834da3b61 100644 --- a/pkg/infra/remotecache/database_storage.go +++ b/pkg/infra/remotecache/database_storage.go @@ -14,14 +14,12 @@ const databaseCacheType = "database" type databaseCache struct { SQLStore db.DB - codec codec log log.Logger } -func newDatabaseCache(sqlstore db.DB, codec codec) *databaseCache { +func newDatabaseCache(sqlstore db.DB) *databaseCache { dc := &databaseCache{ SQLStore: sqlstore, - codec: codec, log: log.New("remotecache.database"), } @@ -54,7 +52,7 @@ func (dc *databaseCache) internalRunGC() { } } -func (dc *databaseCache) GetByteArray(ctx context.Context, key string) ([]byte, error) { +func (dc *databaseCache) Get(ctx context.Context, key string) ([]byte, error) { cacheHit := CacheData{} err := dc.SQLStore.WithDbSession(ctx, func(session *db.Session) error { @@ -85,21 +83,7 @@ func (dc *databaseCache) GetByteArray(ctx context.Context, key string) ([]byte, return cacheHit.Data, err } -func (dc *databaseCache) Get(ctx context.Context, key string) (interface{}, error) { - bytes, err := dc.GetByteArray(ctx, key) - if err != nil { - return nil, err - } - - item := &cachedItem{} - if err = dc.codec.Decode(ctx, bytes, item); err != nil { - return nil, err - } - - return item.Val, err -} - -func (dc *databaseCache) SetByteArray(ctx context.Context, key string, data []byte, expire time.Duration) error { +func (dc *databaseCache) Set(ctx context.Context, key string, data []byte, expire time.Duration) error { return dc.SQLStore.WithDbSession(ctx, func(session *db.Session) error { var expiresInSeconds int64 if expire != 0 { @@ -129,16 +113,6 @@ func (dc *databaseCache) SetByteArray(ctx context.Context, key string, data []by }) } -func (dc *databaseCache) Set(ctx context.Context, key string, value interface{}, expire time.Duration) error { - item := &cachedItem{Val: value} - data, err := dc.codec.Encode(ctx, item) - if err != nil { - return err - } - - return dc.SetByteArray(ctx, key, data, expire) -} - func (dc *databaseCache) Delete(ctx context.Context, key string) error { return dc.SQLStore.WithDbSession(ctx, func(session *db.Session) error { sql := "DELETE FROM cache_data WHERE cache_key=?" diff --git a/pkg/infra/remotecache/database_storage_test.go b/pkg/infra/remotecache/database_storage_test.go index 57769a05647..b3a64e79bb0 100644 --- a/pkg/infra/remotecache/database_storage_test.go +++ b/pkg/infra/remotecache/database_storage_test.go @@ -17,11 +17,10 @@ func TestDatabaseStorageGarbageCollection(t *testing.T) { db := &databaseCache{ SQLStore: sqlstore, - codec: &gobCodec{}, log: log.New("remotecache.database"), } - obj := &CacheableStruct{String: "foolbar"} + obj := []byte("foolbar") // set time.now to 2 weeks ago var err error @@ -66,11 +65,10 @@ func TestSecondSet(t *testing.T) { db := &databaseCache{ SQLStore: sqlstore, - codec: &gobCodec{}, log: log.New("remotecache.database"), } - obj := &CacheableStruct{String: "hey!"} + obj := []byte("hey!") err = db.Set(context.Background(), "killa-gorilla", obj, 0) assert.Equal(t, err, nil) @@ -84,11 +82,10 @@ func TestDatabaseStorageCount(t *testing.T) { db := &databaseCache{ SQLStore: sqlstore, - codec: &gobCodec{}, log: log.New("remotecache.database"), } - obj := &CacheableStruct{String: "foolbar"} + obj := []byte("foolbar") // set time.now to 2 weeks ago var err error diff --git a/pkg/infra/remotecache/memcached_storage.go b/pkg/infra/remotecache/memcached_storage.go index ef3ba900d12..6149c82245d 100644 --- a/pkg/infra/remotecache/memcached_storage.go +++ b/pkg/infra/remotecache/memcached_storage.go @@ -15,14 +15,12 @@ const memcachedCacheType = "memcached" var ErrNotImplemented = errors.New("not implemented") type memcachedStorage struct { - c *memcache.Client - codec codec + c *memcache.Client } -func newMemcachedStorage(opts *setting.RemoteCacheOptions, codec codec) *memcachedStorage { +func newMemcachedStorage(opts *setting.RemoteCacheOptions) *memcachedStorage { return &memcachedStorage{ - c: memcache.New(opts.ConnStr), - codec: codec, + c: memcache.New(opts.ConnStr), } } @@ -34,19 +32,8 @@ func newItem(sid string, data []byte, expire int32) *memcache.Item { } } -// Set sets value to given key in the cache. -func (s *memcachedStorage) Set(ctx context.Context, key string, val interface{}, expires time.Duration) error { - item := &cachedItem{Val: val} - bytes, err := s.codec.Encode(ctx, item) - if err != nil { - return err - } - - return s.SetByteArray(ctx, key, bytes, expires) -} - // SetByteArray stores an byte array in the cache -func (s *memcachedStorage) SetByteArray(ctx context.Context, key string, data []byte, expires time.Duration) error { +func (s *memcachedStorage) Set(ctx context.Context, key string, data []byte, expires time.Duration) error { var expiresInSeconds int64 if expires != 0 { expiresInSeconds = int64(expires) / int64(time.Second) @@ -56,24 +43,8 @@ func (s *memcachedStorage) SetByteArray(ctx context.Context, key string, data [] return s.c.Set(memcachedItem) } -// Get gets value by given key in the cache. -func (s *memcachedStorage) Get(ctx context.Context, key string) (interface{}, error) { - bytes, err := s.GetByteArray(ctx, key) - if err != nil { - return nil, err - } - - item := &cachedItem{} - err = s.codec.Decode(ctx, bytes, item) - if err != nil { - return nil, err - } - - return item.Val, nil -} - // GetByteArray returns the cached value as an byte array -func (s *memcachedStorage) GetByteArray(ctx context.Context, key string) ([]byte, error) { +func (s *memcachedStorage) Get(ctx context.Context, key string) ([]byte, error) { memcachedItem, err := s.c.Get(key) if errors.Is(err, memcache.ErrCacheMiss) { return nil, ErrCacheItemNotFound diff --git a/pkg/infra/remotecache/redis_storage.go b/pkg/infra/remotecache/redis_storage.go index 6d386b592f8..13684530525 100644 --- a/pkg/infra/remotecache/redis_storage.go +++ b/pkg/infra/remotecache/redis_storage.go @@ -16,8 +16,7 @@ import ( const redisCacheType = "redis" type redisStorage struct { - c *redis.Client - codec codec + c *redis.Client } // parseRedisConnStr parses k=v pairs in csv and builds a redis Options object @@ -78,54 +77,22 @@ func parseRedisConnStr(connStr string) (*redis.Options, error) { return options, nil } -func newRedisStorage(opts *setting.RemoteCacheOptions, codec codec) (*redisStorage, error) { +func newRedisStorage(opts *setting.RemoteCacheOptions) (*redisStorage, error) { opt, err := parseRedisConnStr(opts.ConnStr) if err != nil { return nil, err } - return &redisStorage{c: redis.NewClient(opt), codec: codec}, nil -} - -// Set sets value to given key in session. -func (s *redisStorage) Set(ctx context.Context, key string, val interface{}, expires time.Duration) error { - item := &cachedItem{Val: val} - value, err := s.codec.Encode(ctx, item) - if err != nil { - return err - } - - return s.SetByteArray(ctx, key, value, expires) + return &redisStorage{c: redis.NewClient(opt)}, nil } // Set sets value to a given key -func (s *redisStorage) SetByteArray(ctx context.Context, key string, data []byte, expires time.Duration) error { +func (s *redisStorage) Set(ctx context.Context, key string, data []byte, expires time.Duration) error { status := s.c.Set(ctx, key, data, expires) return status.Err() } -// Get gets value by given key in session. -func (s *redisStorage) Get(ctx context.Context, key string) (interface{}, error) { - v, err := s.GetByteArray(ctx, key) - - if err != nil { - if err.Error() == "EOF" { - return nil, ErrCacheItemNotFound - } - return nil, err - } - - item := &cachedItem{} - err = s.codec.Decode(ctx, v, item) - - if err == nil { - return item.Val, nil - } - - return nil, err -} - // GetByteArray returns the value as byte array -func (s *redisStorage) GetByteArray(ctx context.Context, key string) ([]byte, error) { +func (s *redisStorage) Get(ctx context.Context, key string) ([]byte, error) { return s.c.Get(ctx, key).Bytes() } diff --git a/pkg/infra/remotecache/remotecache.go b/pkg/infra/remotecache/remotecache.go index 6ecf712e586..039b962d087 100644 --- a/pkg/infra/remotecache/remotecache.go +++ b/pkg/infra/remotecache/remotecache.go @@ -1,9 +1,7 @@ package remotecache import ( - "bytes" "context" - "encoding/gob" "errors" "time" @@ -30,13 +28,7 @@ const ( func ProvideService(cfg *setting.Cfg, sqlStore db.DB, usageStats usagestats.Service, secretsService secrets.Service) (*RemoteCache, error) { - var codec codec - if cfg.RemoteCacheOptions.Encryption { - codec = &encryptionCodec{secretsService} - } else { - codec = &gobCodec{} - } - client, err := createClient(cfg.RemoteCacheOptions, sqlStore, codec) + client, err := createClient(cfg.RemoteCacheOptions, sqlStore) if err != nil { return nil, err } @@ -69,11 +61,11 @@ func (ds *RemoteCache) getUsageStats(ctx context.Context) (map[string]interface{ // so any struct added to the cache needs to be registered with `remotecache.Register` // ex `remotecache.Register(CacheableStruct{})` type CacheStorage interface { - // GetByteArray gets the cache value as an byte array - GetByteArray(ctx context.Context, key string) ([]byte, error) + // Get gets the cache value as an byte array + Get(ctx context.Context, key string) ([]byte, error) - // SetByteArray saves the value as an byte array. if `expire` is set to zero it will default to 24h - SetByteArray(ctx context.Context, key string, value []byte, expire time.Duration) error + // Set saves the value as an byte array. if `expire` is set to zero it will default to 24h + Set(ctx context.Context, key string, value []byte, expire time.Duration) error // Delete object from cache Delete(ctx context.Context, key string) error @@ -91,18 +83,18 @@ type RemoteCache struct { Cfg *setting.Cfg } -// GetByteArray returns the cached value as an byte array -func (ds *RemoteCache) GetByteArray(ctx context.Context, key string) ([]byte, error) { - return ds.client.GetByteArray(ctx, key) +// Get returns the cached value as an byte array +func (ds *RemoteCache) Get(ctx context.Context, key string) ([]byte, error) { + return ds.client.Get(ctx, key) } -// SetByteArray stored the byte array in the cache -func (ds *RemoteCache) SetByteArray(ctx context.Context, key string, value []byte, expire time.Duration) error { +// Set stored the byte array in the cache +func (ds *RemoteCache) Set(ctx context.Context, key string, value []byte, expire time.Duration) error { if expire == 0 { expire = defaultMaxCacheExpiration } - return ds.client.SetByteArray(ctx, key, value, expire) + return ds.client.Set(ctx, key, value, expire) } // Delete object from cache @@ -127,14 +119,14 @@ func (ds *RemoteCache) Run(ctx context.Context) error { return ctx.Err() } -func createClient(opts *setting.RemoteCacheOptions, sqlstore db.DB, codec codec) (cache CacheStorage, err error) { +func createClient(opts *setting.RemoteCacheOptions, sqlstore db.DB) (cache CacheStorage, err error) { switch opts.Name { case redisCacheType: - cache, err = newRedisStorage(opts, codec) + cache, err = newRedisStorage(opts) case memcachedCacheType: - cache = newMemcachedStorage(opts, codec) + cache = newMemcachedStorage(opts) case databaseCacheType: - cache = newDatabaseCache(sqlstore, codec) + cache = newDatabaseCache(sqlstore) default: return nil, ErrInvalidCacheType } @@ -144,61 +136,45 @@ func createClient(opts *setting.RemoteCacheOptions, sqlstore db.DB, codec codec) if opts.Prefix != "" { cache = &prefixCacheStorage{cache: cache, prefix: opts.Prefix} } + + if opts.Encryption { + cache = &encryptedCacheStorage{cache: cache} + } return cache, nil } -// Register records a type, identified by a value for that type, under its -// internal type name. That name will identify the concrete type of a value -// sent or received as an interface variable. Only types that will be -// transferred as implementations of interface values need to be registered. -// Expecting to be used only during initialization, it panics if the mapping -// between types and names is not a bijection. -func Register(value interface{}) { - gob.Register(value) +type encryptedCacheStorage struct { + cache CacheStorage + secretsService encryptionService } -type cachedItem struct { - Val interface{} +type encryptionService interface { + Encrypt(ctx context.Context, payload []byte, opt secrets.EncryptionOptions) ([]byte, error) + Decrypt(ctx context.Context, payload []byte) ([]byte, error) } -type codec interface { - Encode(context.Context, *cachedItem) ([]byte, error) - Decode(context.Context, []byte, *cachedItem) error -} - -type gobCodec struct{} - -func (c *gobCodec) Encode(_ context.Context, item *cachedItem) ([]byte, error) { - buf := bytes.NewBuffer(nil) - err := gob.NewEncoder(buf).Encode(item) - return buf.Bytes(), err -} - -func (c *gobCodec) Decode(_ context.Context, data []byte, out *cachedItem) error { - buf := bytes.NewBuffer(data) - return gob.NewDecoder(buf).Decode(&out) -} - -type encryptionCodec struct { - secretsService secrets.Service -} - -func (c *encryptionCodec) Encode(ctx context.Context, item *cachedItem) ([]byte, error) { - buf := bytes.NewBuffer(nil) - err := gob.NewEncoder(buf).Encode(item) +func (pcs *encryptedCacheStorage) Get(ctx context.Context, key string) ([]byte, error) { + data, err := pcs.cache.Get(ctx, key) if err != nil { return nil, err } - return c.secretsService.Encrypt(ctx, buf.Bytes(), secrets.WithoutScope()) -} -func (c *encryptionCodec) Decode(ctx context.Context, data []byte, out *cachedItem) error { - decrypted, err := c.secretsService.Decrypt(ctx, data) + return pcs.secretsService.Decrypt(ctx, data) +} +func (pcs *encryptedCacheStorage) Set(ctx context.Context, key string, value []byte, expire time.Duration) error { + encrypted, err := pcs.secretsService.Encrypt(ctx, value, secrets.WithoutScope()) if err != nil { return err } - buf := bytes.NewBuffer(decrypted) - return gob.NewDecoder(buf).Decode(&out) + + return pcs.cache.Set(ctx, key, encrypted, expire) +} +func (pcs *encryptedCacheStorage) Delete(ctx context.Context, key string) error { + return pcs.cache.Delete(ctx, key) +} + +func (pcs *encryptedCacheStorage) Count(ctx context.Context, prefix string) (int64, error) { + return pcs.cache.Count(ctx, prefix) } type prefixCacheStorage struct { @@ -206,16 +182,16 @@ type prefixCacheStorage struct { prefix string } -func (pcs *prefixCacheStorage) GetByteArray(ctx context.Context, key string) ([]byte, error) { - return pcs.cache.GetByteArray(ctx, pcs.prefix+key) +func (pcs *prefixCacheStorage) Get(ctx context.Context, key string) ([]byte, error) { + return pcs.cache.Get(ctx, pcs.prefix+key) } -func (pcs *prefixCacheStorage) SetByteArray(ctx context.Context, key string, value []byte, expire time.Duration) error { - return pcs.cache.SetByteArray(ctx, pcs.prefix+key, value, expire) +func (pcs *prefixCacheStorage) Set(ctx context.Context, key string, value []byte, expire time.Duration) error { + return pcs.cache.Set(ctx, pcs.prefix+key, value, expire) } func (pcs *prefixCacheStorage) Delete(ctx context.Context, key string) error { return pcs.cache.Delete(ctx, pcs.prefix+key) } func (pcs *prefixCacheStorage) Count(ctx context.Context, prefix string) (int64, error) { - return pcs.cache.Count(ctx, pcs.prefix) + return pcs.cache.Count(ctx, pcs.prefix+prefix) } diff --git a/pkg/infra/remotecache/remotecache_test.go b/pkg/infra/remotecache/remotecache_test.go index 585154af647..7c99f3d6a90 100644 --- a/pkg/infra/remotecache/remotecache_test.go +++ b/pkg/infra/remotecache/remotecache_test.go @@ -9,21 +9,12 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/setting" ) -type CacheableStruct struct { - String string - Int64 int64 -} - -func init() { - Register(CacheableStruct{}) -} - func createTestClient(t *testing.T, opts *setting.RemoteCacheOptions, sqlstore db.DB) CacheStorage { t.Helper() @@ -49,7 +40,7 @@ func TestCachedBasedOnConfig(t *testing.T) { } func TestInvalidCacheTypeReturnsError(t *testing.T) { - _, err := createClient(&setting.RemoteCacheOptions{Name: "invalid"}, nil, &gobCodec{}) + _, err := createClient(&setting.RemoteCacheOptions{Name: "invalid"}, nil) assert.Equal(t, err, ErrInvalidCacheType) } @@ -68,13 +59,13 @@ func runCountTestsForClient(t *testing.T, opts *setting.RemoteCacheOptions, sqls t.Run("can count items", func(t *testing.T) { cacheableValue := []byte("hej hej") - err := client.SetByteArray(context.Background(), "pref-key1", cacheableValue, 0) + err := client.Set(context.Background(), "pref-key1", cacheableValue, 0) require.NoError(t, err) - err = client.SetByteArray(context.Background(), "pref-key2", cacheableValue, 0) + err = client.Set(context.Background(), "pref-key2", cacheableValue, 0) require.NoError(t, err) - err = client.SetByteArray(context.Background(), "key3-not-pref", cacheableValue, 0) + err = client.Set(context.Background(), "key3-not-pref", cacheableValue, 0) require.NoError(t, err) n, errC := client.Count(context.Background(), "pref-") @@ -92,10 +83,10 @@ func runCountTestsForClient(t *testing.T, opts *setting.RemoteCacheOptions, sqls func canPutGetAndDeleteCachedObjects(t *testing.T, client CacheStorage) { dataToCache := []byte("some bytes") - err := client.SetByteArray(context.Background(), "key1", dataToCache, 0) + err := client.Set(context.Background(), "key1", dataToCache, 0) assert.Equal(t, err, nil, "expected nil. got: ", err) - data, err := client.GetByteArray(context.Background(), "key1") + data, err := client.Get(context.Background(), "key1") assert.Equal(t, err, nil) assert.Equal(t, string(data), "some bytes") @@ -103,21 +94,21 @@ func canPutGetAndDeleteCachedObjects(t *testing.T, client CacheStorage) { err = client.Delete(context.Background(), "key1") assert.Equal(t, err, nil) - _, err = client.GetByteArray(context.Background(), "key1") + _, err = client.Get(context.Background(), "key1") assert.Equal(t, err, ErrCacheItemNotFound) } func canNotFetchExpiredItems(t *testing.T, client CacheStorage) { dataToCache := []byte("some bytes") - err := client.SetByteArray(context.Background(), "key1", dataToCache, time.Second) + err := client.Set(context.Background(), "key1", dataToCache, time.Second) assert.Equal(t, err, nil) // not sure how this can be avoided when testing redis/memcached :/ <-time.After(time.Second + time.Millisecond) // should not be able to read that value since its expired - _, err = client.GetByteArray(context.Background(), "key1") + _, err = client.Get(context.Background(), "key1") assert.Equal(t, err, ErrCacheItemNotFound) } @@ -140,26 +131,91 @@ func TestCollectUsageStats(t *testing.T) { } func TestCachePrefix(t *testing.T) { - db := db.InitTestDB(t) - cache := &databaseCache{ - SQLStore: db, - log: log.New("remotecache.database"), - codec: &gobCodec{}, - } + cache := NewFakeCacheStorage() prefixCache := &prefixCacheStorage{cache: cache, prefix: "test/"} // Set a value (with a prefix) - err := prefixCache.SetByteArray(context.Background(), "foo", []byte("bar"), time.Hour) + err := prefixCache.Set(context.Background(), "foo", []byte("bar"), time.Hour) require.NoError(t, err) // Get a value (with a prefix) - v, err := prefixCache.GetByteArray(context.Background(), "foo") + v, err := prefixCache.Get(context.Background(), "foo") require.NoError(t, err) require.Equal(t, "bar", string(v)) // Get a value directly from the underlying cache, ensure the prefix is in the key - v, err = cache.GetByteArray(context.Background(), "test/foo") + v, err = cache.Get(context.Background(), "test/foo") require.NoError(t, err) require.Equal(t, "bar", string(v)) // Get a value directly from the underlying cache without a prefix, should not be there _, err = cache.Get(context.Background(), "foo") require.Error(t, err) } + +func TestEncryptedCache(t *testing.T) { + cache := NewFakeCacheStorage() + encryptedCache := &encryptedCacheStorage{cache: cache, secretsService: &fakeSecretsService{}} + + // Set a value in the encrypted cache + err := encryptedCache.Set(context.Background(), "foo", []byte("bar"), time.Hour) + require.NoError(t, err) + + // make sure the stored value is not equal to input + v, err := cache.Get(context.Background(), "foo") + require.NoError(t, err) + require.NotEqual(t, "bar", string(v)) + + // make sure the returned value is the same as orignial + v, err = encryptedCache.Get(context.Background(), "foo") + require.NoError(t, err) + require.Equal(t, "bar", string(v)) +} + +type fakeCacheStorage struct { + storage map[string][]byte +} + +func (fcs fakeCacheStorage) Set(_ context.Context, key string, value []byte, exp time.Duration) error { + fcs.storage[key] = value + return nil +} + +func (fcs fakeCacheStorage) Get(_ context.Context, key string) ([]byte, error) { + value, exist := fcs.storage[key] + if !exist { + return nil, ErrCacheItemNotFound + } + + return value, nil +} + +func (fcs fakeCacheStorage) Delete(_ context.Context, key string) error { + delete(fcs.storage, key) + return nil +} + +func (fcs fakeCacheStorage) Count(_ context.Context, prefix string) (int64, error) { + return int64(len(fcs.storage)), nil +} + +func NewFakeCacheStorage() CacheStorage { + return fakeCacheStorage{ + storage: map[string][]byte{}, + } +} + +type fakeSecretsService struct{} + +func (f fakeSecretsService) Encrypt(_ context.Context, payload []byte, _ secrets.EncryptionOptions) ([]byte, error) { + return f.reverse(payload), nil +} + +func (f fakeSecretsService) Decrypt(_ context.Context, payload []byte) ([]byte, error) { + return f.reverse(payload), nil +} + +func (f fakeSecretsService) reverse(input []byte) []byte { + r := []rune(string(input)) + for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { + r[i], r[j] = r[j], r[i] + } + return []byte(string(r)) +} diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 0331a31b82e..fcbe9edc190 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -615,7 +615,7 @@ func TestMiddlewareContext(t *testing.T) { require.NoError(t, err) key := fmt.Sprintf(authproxy.CachePrefix, h) userIdBytes := []byte(strconv.FormatInt(userID, 10)) - err = sc.remoteCacheService.SetByteArray(context.Background(), key, userIdBytes, 0) + err = sc.remoteCacheService.Set(context.Background(), key, userIdBytes, 0) require.NoError(t, err) sc.fakeReq("GET", "/") diff --git a/pkg/services/anonymous/anonimpl/impl.go b/pkg/services/anonymous/anonimpl/impl.go index c6677086e49..df055628821 100644 --- a/pkg/services/anonymous/anonimpl/impl.go +++ b/pkg/services/anonymous/anonimpl/impl.go @@ -96,5 +96,5 @@ func (a *AnonSessionService) TagSession(ctx context.Context, httpReq *http.Reque a.localCache.SetDefault(key, struct{}{}) - return a.remoteCache.SetByteArray(ctx, key, []byte(key), thirtyDays) + return a.remoteCache.Set(ctx, key, []byte(key), thirtyDays) } diff --git a/pkg/services/auth/jwt/key_sets.go b/pkg/services/auth/jwt/key_sets.go index 6fc366191f0..05b746f4ce0 100644 --- a/pkg/services/auth/jwt/key_sets.go +++ b/pkg/services/auth/jwt/key_sets.go @@ -171,7 +171,7 @@ func (ks *keySetHTTP) getJWKS(ctx context.Context) (keySetJWKS, error) { var jwks keySetJWKS if ks.cacheExpiration > 0 { - if val, err := ks.cache.GetByteArray(ctx, ks.cacheKey); err == nil { + if val, err := ks.cache.Get(ctx, ks.cacheKey); err == nil { err := json.Unmarshal(val, &jwks) return jwks, err } @@ -200,7 +200,7 @@ func (ks *keySetHTTP) getJWKS(ctx context.Context) (keySetJWKS, error) { } if ks.cacheExpiration > 0 { - err = ks.cache.SetByteArray(ctx, ks.cacheKey, jsonBuf.Bytes(), ks.cacheExpiration) + err = ks.cache.Set(ctx, ks.cacheKey, jsonBuf.Bytes(), ks.cacheExpiration) } return jwks, err } diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go index a53fb754edb..c7384eb898b 100644 --- a/pkg/services/authn/clients/proxy.go +++ b/pkg/services/authn/clients/proxy.go @@ -50,8 +50,8 @@ func ProvideProxy(cfg *setting.Cfg, cache proxyCache, userSrv user.Service, clie } type proxyCache interface { - GetByteArray(ctx context.Context, key string) ([]byte, error) - SetByteArray(ctx context.Context, key string, value []byte, expire time.Duration) error + Get(ctx context.Context, key string) ([]byte, error) + Set(ctx context.Context, key string, value []byte, expire time.Duration) error } type Proxy struct { @@ -83,7 +83,7 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden if ok { // See if we have cached the user id, in that case we can fetch the signed-in user and skip sync. // Error here means that we could not find anything in cache, so we can proceed as usual - if entry, err := c.cache.GetByteArray(ctx, cacheKey); err == nil { + if entry, err := c.cache.Get(ctx, cacheKey); err == nil { uid := int64(binary.LittleEndian.Uint64(entry)) usr, err := c.userSrv.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{ @@ -138,7 +138,7 @@ func (c *Proxy) Hook(ctx context.Context, identity *authn.Identity, r *authn.Req c.log.FromContext(ctx).Debug("Cache proxy user", "userId", id) bytes := make([]byte, 8) binary.LittleEndian.PutUint64(bytes, uint64(id)) - if err := c.cache.SetByteArray(ctx, identity.ClientParams.CacheAuthProxyKey, bytes, time.Duration(c.cfg.AuthProxySyncTTL)*time.Minute); err != nil { + if err := c.cache.Set(ctx, identity.ClientParams.CacheAuthProxyKey, bytes, time.Duration(c.cfg.AuthProxySyncTTL)*time.Minute); err != nil { c.log.Warn("failed to cache proxy user", "error", err, "userId", id) } diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go index a0f39ec0425..a0b19dc1a0d 100644 --- a/pkg/services/authn/clients/proxy_test.go +++ b/pkg/services/authn/clients/proxy_test.go @@ -181,10 +181,10 @@ type fakeCache struct { expectedItem []byte } -func (f fakeCache) GetByteArray(ctx context.Context, key string) ([]byte, error) { +func (f fakeCache) Get(ctx context.Context, key string) ([]byte, error) { return f.expectedItem, f.expectedErr } -func (f fakeCache) SetByteArray(ctx context.Context, key string, value []byte, expire time.Duration) error { +func (f fakeCache) Set(ctx context.Context, key string, value []byte, expire time.Duration) error { return f.expectedErr } diff --git a/pkg/services/contexthandler/auth_proxy_test.go b/pkg/services/contexthandler/auth_proxy_test.go index 2a233b97b62..124fa98a980 100644 --- a/pkg/services/contexthandler/auth_proxy_test.go +++ b/pkg/services/contexthandler/auth_proxy_test.go @@ -58,7 +58,7 @@ func TestInitContextWithAuthProxy_CachedInvalidUserID(t *testing.T) { t.Logf("Injecting stale user ID in cache with key %q", key) userIdPayload := []byte(strconv.FormatInt(int64(33), 10)) - err = svc.RemoteCache.SetByteArray(context.Background(), key, userIdPayload, 0) + err = svc.RemoteCache.Set(context.Background(), key, userIdPayload, 0) require.NoError(t, err) authEnabled := svc.initContextWithAuthProxy(ctx, orgID) @@ -67,7 +67,7 @@ func TestInitContextWithAuthProxy_CachedInvalidUserID(t *testing.T) { require.Equal(t, userID, ctx.SignedInUser.UserID) require.True(t, ctx.IsSignedIn) - cachedByteArray, err := svc.RemoteCache.GetByteArray(context.Background(), key) + cachedByteArray, err := svc.RemoteCache.Get(context.Background(), key) require.NoError(t, err) cacheUserId, err := strconv.ParseInt(string(cachedByteArray), 10, 64) diff --git a/pkg/services/contexthandler/authproxy/authproxy.go b/pkg/services/contexthandler/authproxy/authproxy.go index 847ae84c0b9..5f42274e6c7 100644 --- a/pkg/services/contexthandler/authproxy/authproxy.go +++ b/pkg/services/contexthandler/authproxy/authproxy.go @@ -191,7 +191,7 @@ func (auth *AuthProxy) getUserViaCache(reqCtx *contextmodel.ReqContext) (int64, return 0, err } auth.logger.Debug("Getting user ID via auth cache", "cacheKey", cacheKey) - cachedValue, err := auth.remoteCache.GetByteArray(reqCtx.Req.Context(), cacheKey) + cachedValue, err := auth.remoteCache.Get(reqCtx.Req.Context(), cacheKey) if err != nil { return 0, err } @@ -353,7 +353,7 @@ func (auth *AuthProxy) Remember(reqCtx *contextmodel.ReqContext, id int64) error } // Check if user already in cache - cachedValue, err := auth.remoteCache.GetByteArray(reqCtx.Req.Context(), key) + cachedValue, err := auth.remoteCache.Get(reqCtx.Req.Context(), key) if err == nil && len(cachedValue) != 0 { return nil } @@ -361,7 +361,7 @@ func (auth *AuthProxy) Remember(reqCtx *contextmodel.ReqContext, id int64) error expiration := time.Duration(auth.cfg.AuthProxySyncTTL) * time.Minute userIdPayload := []byte(strconv.FormatInt(id, 10)) - if err := auth.remoteCache.SetByteArray(reqCtx.Req.Context(), key, userIdPayload, expiration); err != nil { + if err := auth.remoteCache.Set(reqCtx.Req.Context(), key, userIdPayload, expiration); err != nil { return err } diff --git a/pkg/services/contexthandler/authproxy/authproxy_test.go b/pkg/services/contexthandler/authproxy/authproxy_test.go index 361d0972a5c..57f3f55b170 100644 --- a/pkg/services/contexthandler/authproxy/authproxy_test.go +++ b/pkg/services/contexthandler/authproxy/authproxy_test.go @@ -61,7 +61,7 @@ func TestMiddlewareContext(t *testing.T) { require.NoError(t, err) key := fmt.Sprintf(CachePrefix, h) userIdPayload := []byte(strconv.FormatInt(id, 10)) - err = cache.SetByteArray(context.Background(), key, userIdPayload, 0) + err = cache.Set(context.Background(), key, userIdPayload, 0) require.NoError(t, err) // Set up the middleware auth, reqCtx := prepareMiddleware(t, cache, nil) @@ -84,7 +84,7 @@ func TestMiddlewareContext(t *testing.T) { require.NoError(t, err) key := fmt.Sprintf(CachePrefix, h) userIdPayload := []byte(strconv.FormatInt(id, 10)) - err = cache.SetByteArray(context.Background(), key, userIdPayload, 0) + err = cache.Set(context.Background(), key, userIdPayload, 0) require.NoError(t, err) auth, reqCtx := prepareMiddleware(t, cache, func(req *http.Request, cfg *setting.Cfg) { diff --git a/pkg/services/rendering/auth.go b/pkg/services/rendering/auth.go index 307430333a8..32ef4e26d79 100644 --- a/pkg/services/rendering/auth.go +++ b/pkg/services/rendering/auth.go @@ -21,7 +21,7 @@ type RenderUser struct { } func (rs *RenderingService) GetRenderUser(ctx context.Context, key string) (*RenderUser, bool) { - val, err := rs.RemoteCacheService.GetByteArray(ctx, fmt.Sprintf(renderKeyPrefix, key)) + val, err := rs.RemoteCacheService.Get(ctx, fmt.Sprintf(renderKeyPrefix, key)) if err != nil { rs.log.Error("Failed to get render key from cache", "error", err) } @@ -46,7 +46,7 @@ func setRenderKey(cache *remotecache.RemoteCache, ctx context.Context, opts Auth return err } - return cache.SetByteArray(ctx, fmt.Sprintf(renderKeyPrefix, renderKey), buf.Bytes(), expiry) + return cache.Set(ctx, fmt.Sprintf(renderKeyPrefix, renderKey), buf.Bytes(), expiry) } func generateAndSetRenderKey(cache *remotecache.RemoteCache, ctx context.Context, opts AuthOpts, expiry time.Duration) (string, error) { From a8f201f8ab1a5500671a1f70a582c701fbc94fc7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Mar 2023 14:00:25 +0100 Subject: [PATCH 15/33] Update dependency rimraf to v4.4.0 (#64601) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-e2e/package.json | 2 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-schema/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 22 ++++++++++----------- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index a696c38a518..9028fbb7caf 100644 --- a/package.json +++ b/package.json @@ -229,7 +229,7 @@ "react-simple-compat": "1.2.3", "react-test-renderer": "17.0.2", "redux-mock-store": "1.5.4", - "rimraf": "4.2.0", + "rimraf": "4.4.0", "rudder-sdk-js": "2.25.0", "sass": "1.58.3", "sass-loader": "13.2.0", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 9d3d21e7ce8..ff16037a19c 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -83,7 +83,7 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-test-renderer": "17.0.2", - "rimraf": "4.2.0", + "rimraf": "4.4.0", "rollup": "2.79.1", "rollup-plugin-dts": "^5.0.0", "rollup-plugin-esbuild": "5.0.0", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 4ebfcd108ec..3035cfb0ba5 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -43,7 +43,7 @@ "@rollup/plugin-node-resolve": "15.0.1", "@types/node": "18.14.6", "esbuild": "0.16.17", - "rimraf": "4.2.0", + "rimraf": "4.4.0", "rollup": "2.79.1", "rollup-plugin-dts": "^5.0.0", "rollup-plugin-esbuild": "5.0.0", diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 29b3996ffd3..6c720439698 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -77,7 +77,7 @@ "lodash": "4.17.21", "mocha": "10.2.0", "resolve-bin": "1.0.1", - "rimraf": "4.2.0", + "rimraf": "4.4.0", "tracelib": "1.0.1", "ts-loader": "8.4.0", "tslib": "2.5.0", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index c775a7b7a39..0565cd2747b 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -67,7 +67,7 @@ "lodash": "4.17.21", "react": "17.0.2", "react-dom": "17.0.2", - "rimraf": "4.2.0", + "rimraf": "4.4.0", "rollup": "2.79.1", "rollup-plugin-dts": "^5.0.0", "rollup-plugin-esbuild": "5.0.0", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 1d8c6522569..542b6a1428a 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -41,7 +41,7 @@ "@rollup/plugin-json": "5.0.1", "@rollup/plugin-node-resolve": "15.0.1", "esbuild": "0.16.17", - "rimraf": "4.2.0", + "rimraf": "4.4.0", "rollup": "2.79.1", "rollup-plugin-dts": "^5.0.0", "rollup-plugin-esbuild": "5.0.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 2ad949e3324..c92286e62b4 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -175,7 +175,7 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-test-renderer": "17.0.2", - "rimraf": "4.2.0", + "rimraf": "4.4.0", "rollup": "2.79.1", "rollup-plugin-dts": "^5.0.0", "rollup-plugin-esbuild": "5.0.0", diff --git a/yarn.lock b/yarn.lock index b72d883bfb7..c7d83ff4868 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4956,7 +4956,7 @@ __metadata: react-test-renderer: 17.0.2 react-use: 17.4.0 regenerator-runtime: 0.13.11 - rimraf: 4.2.0 + rimraf: 4.4.0 rollup: 2.79.1 rollup-plugin-dts: ^5.0.0 rollup-plugin-esbuild: 5.0.0 @@ -4983,7 +4983,7 @@ __metadata: "@rollup/plugin-node-resolve": 15.0.1 "@types/node": 18.14.6 esbuild: 0.16.17 - rimraf: 4.2.0 + rimraf: 4.4.0 rollup: 2.79.1 rollup-plugin-dts: ^5.0.0 rollup-plugin-esbuild: 5.0.0 @@ -5031,7 +5031,7 @@ __metadata: lodash: 4.17.21 mocha: 10.2.0 resolve-bin: 1.0.1 - rimraf: 4.2.0 + rimraf: 4.4.0 rollup: 2.79.1 rollup-plugin-dts: ^5.0.0 rollup-plugin-esbuild: 5.0.0 @@ -5190,7 +5190,7 @@ __metadata: lodash: 4.17.21 react: 17.0.2 react-dom: 17.0.2 - rimraf: 4.2.0 + rimraf: 4.4.0 rollup: 2.79.1 rollup-plugin-dts: ^5.0.0 rollup-plugin-esbuild: 5.0.0 @@ -5230,7 +5230,7 @@ __metadata: "@rollup/plugin-json": 5.0.1 "@rollup/plugin-node-resolve": 15.0.1 esbuild: 0.16.17 - rimraf: 4.2.0 + rimraf: 4.4.0 rollup: 2.79.1 rollup-plugin-dts: ^5.0.0 rollup-plugin-esbuild: 5.0.0 @@ -5462,7 +5462,7 @@ __metadata: react-transition-group: 4.4.5 react-use: 17.4.0 react-window: 1.8.8 - rimraf: 4.2.0 + rimraf: 4.4.0 rollup: 2.79.1 rollup-plugin-dts: ^5.0.0 rollup-plugin-esbuild: 5.0.0 @@ -22387,7 +22387,7 @@ __metadata: redux-thunk: 2.4.2 regenerator-runtime: 0.13.11 reselect: 4.1.7 - rimraf: 4.2.0 + rimraf: 4.4.0 rst2html: "github:thoward/rst2html#990cb89f2a300cdd9151790be377c4c0840df809" rudder-sdk-js: 2.25.0 rxjs: 7.8.0 @@ -34600,14 +34600,14 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:4.2.0": - version: 4.2.0 - resolution: "rimraf@npm:4.2.0" +"rimraf@npm:4.4.0": + version: 4.4.0 + resolution: "rimraf@npm:4.4.0" dependencies: glob: ^9.2.0 bin: rimraf: dist/cjs/src/bin.js - checksum: 885baaec6d8e3a771bb3060f110c29070073a47fdcb26775ae165703173e79e845974dc8ed07e8eb4c3c85f126777968febdb385d0c274ad3b167db326e7f82e + checksum: 0cedaf9d138589d1bb0ab851f05804c6d30827aa66563472b04ab76245f83537e23e7b94f1f79ea6c368c0d84a18fcde6a756fca3a44c967e08792671b3a0a6e languageName: node linkType: hard From 2f55911fa3163818308bc4f5cb7002a918da0619 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Fri, 10 Mar 2023 13:04:51 +0000 Subject: [PATCH 16/33] Fix: Top table rendering and update docs (#64497) Fix flame graph in test data and update docs --- docs/sources/datasources/testdata/_index.md | 2 ++ .../components/TopTable/FlameGraphTopTableContainer.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/sources/datasources/testdata/_index.md b/docs/sources/datasources/testdata/_index.md index d81f41d6f85..de1e9953e0d 100644 --- a/docs/sources/datasources/testdata/_index.md +++ b/docs/sources/datasources/testdata/_index.md @@ -60,6 +60,7 @@ You can assign an **Alias** to each scenario, and many have their own options th - **CSV Metric Values** - **Datapoints Outside Range** - **Exponential heatmap bucket data** +- **Flame Graph** - **Grafana API** - **Grafana Live** - **Linear heatmap bucket data** @@ -77,6 +78,7 @@ You can assign an **Alias** to each scenario, and many have their own options th - **Slow Query** - **Streaming Client** - **Table Static** +- **Trace** - **USA generated data** ## Import a pre-configured dashboard diff --git a/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.tsx b/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.tsx index 4b75ba35a2a..b3ac0f1cbf8 100644 --- a/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.tsx +++ b/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.tsx @@ -45,7 +45,7 @@ const FlameGraphTopTableContainer = ({ let label, self, value; let table: { [key: string]: TableData } = {}; - if (data.fields.length === 6) { + if (data.fields.length > 3) { const valueValues = data.fields[1].values; const selfValues = data.fields[2].values; const labelValues = data.fields[3].values; From a05cb1e78e77fefad7d474001f59b38819904b1f Mon Sep 17 00:00:00 2001 From: William Assis <35489495+gassiss@users.noreply.github.com> Date: Fri, 10 Mar 2023 08:18:12 -0500 Subject: [PATCH 17/33] Frontend: Fix broken links in /plugins when pathname has a trailing slash (#64348) Fix broken links in /plugins when pathname has a trailing slash --- public/app/features/plugins/admin/components/PluginList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/plugins/admin/components/PluginList.tsx b/public/app/features/plugins/admin/components/PluginList.tsx index 9e12fbf3824..afa87074e14 100644 --- a/public/app/features/plugins/admin/components/PluginList.tsx +++ b/public/app/features/plugins/admin/components/PluginList.tsx @@ -18,8 +18,8 @@ interface Props { export const PluginList = ({ plugins, displayMode }: Props) => { const isList = displayMode === PluginListDisplayMode.List; const styles = useStyles2(getStyles); - const location = useLocation(); - const pathName = config.appSubUrl + location.pathname; + const { pathname } = useLocation(); + const pathName = config.appSubUrl + (pathname.endsWith('/') ? pathname.slice(0, -1) : pathname); return (
From 0a4d9f01e87493871b3ce9d0ab3665134f5c15a7 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 10 Mar 2023 05:40:38 -0800 Subject: [PATCH 18/33] Chore: Use latest version of scenes (#64609) --- package.json | 2 +- yarn.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 9028fbb7caf..c348fe55688 100644 --- a/package.json +++ b/package.json @@ -267,7 +267,7 @@ "@grafana/lezer-logql": "0.1.2", "@grafana/monaco-logql": "^0.0.7", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "^0.0.16", + "@grafana/scenes": "^0.0.18", "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", "@kusto/monaco-kusto": "5.3.6", diff --git a/yarn.lock b/yarn.lock index c7d83ff4868..a7c8bef8c1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4993,14 +4993,14 @@ __metadata: languageName: unknown linkType: soft -"@grafana/e2e-selectors@npm:canary": - version: 9.4.0-96193pre - resolution: "@grafana/e2e-selectors@npm:9.4.0-96193pre" +"@grafana/e2e-selectors@npm:^9.4.3": + version: 9.4.3 + resolution: "@grafana/e2e-selectors@npm:9.4.3" dependencies: "@grafana/tsconfig": ^1.2.0-rc1 tslib: 2.4.1 typescript: 4.8.4 - checksum: 1ce663607c0c816ec02cad33d80c38731c4dfafdc7d1693baa989b8eff6588e3cc1d8a031c4f30f888bbd8aa8768cce7c5dd4cb2455306040f39dcac933ceae4 + checksum: 85a88cdf4adb643ff863b15f96fc6c04ecb7567c27cc526a00c157eb02575e55adc1e7701d58e1b48f00f24951c332fbb191cd2b5a8e74cd0c545543777e82af languageName: node linkType: hard @@ -5207,17 +5207,17 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:^0.0.16": - version: 0.0.16 - resolution: "@grafana/scenes@npm:0.0.16" +"@grafana/scenes@npm:^0.0.18": + version: 0.0.18 + resolution: "@grafana/scenes@npm:0.0.18" dependencies: - "@grafana/e2e-selectors": canary + "@grafana/e2e-selectors": ^9.4.3 "@grafana/experimental": 1.0.1 react-grid-layout: 1.3.4 react-use: 17.4.0 react-virtualized-auto-sizer: 1.0.7 uuid: ^9.0.0 - checksum: c6cf9f1571309da4ed0fb5046f8abf18b324e9847890c54307ca093610760d52fdb44002fb1db8e81abca6371121a0623a2f44c9828ef80b11d8d189993f457a + checksum: f863cbf410d71e579cd4e343fb7b8ad014bc66a250f4d31f9d43c43fe8e97f5b6e3e8d01ed69818c347b3db31b6ecb153d1dc7a0e2946d360e7e1806eb6f5d8c languageName: node linkType: hard @@ -22130,7 +22130,7 @@ __metadata: "@grafana/lezer-logql": 0.1.2 "@grafana/monaco-logql": ^0.0.7 "@grafana/runtime": "workspace:*" - "@grafana/scenes": ^0.0.16 + "@grafana/scenes": ^0.0.18 "@grafana/schema": "workspace:*" "@grafana/toolkit": "workspace:*" "@grafana/tsconfig": ^1.2.0-rc1 From 73ce20ab48ff9e8e97ae021ba8da3457eee3402e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 10 Mar 2023 14:41:46 +0100 Subject: [PATCH 19/33] Table Panel: Add ability to use text color for value or hide value in gauge cell (#61477) * BarGauge: New value options * Fix typings for cell options, add new value mode option for bar gauge cells * Add BarGauge panel option, tests, and update test dashboard * Updated * Added default * Goodbye trusty console.log * Update * Merge changes from main * Update docs * Add valuemode doc changes * Update gdev dashboard * Update valueMode symbol name to valueDisplayMode * Use Enums as Opposed to literals, don't calculate values when hidden * Remove double import * Fix tests * One more test fix * Remove erroneous targets field, fix type of maxDataPoints * Strip nulls and add index field to Thresholds * Gen cue * remove bad targets again * Fixes --------- Co-authored-by: Kyle Cunningham Co-authored-by: sam boyer --- .../panel-bargauge/panel_tests_bar_gauge.json | 1382 ++++++++--- .../panel-table/table_tests_new.json | 2127 ++++++++--------- .../bargaugepanelcfg/schema-reference.md | 1 + .../kinds/core/dashboard/schema-reference.md | 11 +- .../visualizations/table/index.md | 16 + kinds/dashboard/dashboard_kind.cue | 6 +- .../grafana-schema/src/common/common.gen.ts | 10 + .../grafana-schema/src/common/mudball.cue | 3 + packages/grafana-schema/src/common/table.cue | 1 + .../raw/dashboard/x/dashboard_types.gen.ts | 4 + .../src/components/BarGauge/BarGauge.test.tsx | 53 +- .../src/components/BarGauge/BarGauge.tsx | 71 +- .../src/components/Table/BarGaugeCell.tsx | 30 +- pkg/kinds/dashboard/dashboard_types_gen.go | 3 + pkg/kindsys/report.json | 2 +- .../api/alerting/api_alertmanager_test.go | 3 +- .../panel/bargauge/BarGaugePanel.test.tsx | 3 +- .../plugins/panel/bargauge/BarGaugePanel.tsx | 1 + public/app/plugins/panel/bargauge/module.tsx | 14 +- .../app/plugins/panel/bargauge/panelcfg.cue | 1 + .../plugins/panel/bargauge/panelcfg.gen.ts | 2 + .../panel/table/TableCellOptionEditor.tsx | 17 +- .../table/cells/BarGaugeCellOptionsEditor.tsx | 54 +- .../ColorBackgroundCellOptionsEditor.tsx | 10 +- public/app/plugins/panel/table/module.tsx | 4 +- 25 files changed, 2259 insertions(+), 1570 deletions(-) diff --git a/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge.json b/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge.json index 6230bb6c305..9366c4dc238 100644 --- a/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge.json +++ b/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge.json @@ -3,24 +3,74 @@ "list": [ { "builtIn": 1, - "datasource": "-- Grafana --", + "datasource": { + "type": "datasource", + "uid": "grafana" + }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, "type": "dashboard" } ] }, "editable": true, - "gnetId": null, + "fiscalYearStartMonth": 0, "graphTooltip": 0, "links": [], + "liveNow": false, "panels": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 20 + }, + { + "color": "orange", + "index": 2, + "value": 40 + }, + { + "color": "red", + "index": 3, + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, "gridPos": { "h": 7, - "w": 6, + "w": 9, "x": 0, "y": 0 }, @@ -28,789 +78,1333 @@ "links": [], "options": { "displayMode": "gradient", - "fieldOptions": { - "calcs": ["mean"], - "defaults": { - "decimals": null, - "max": 100, - "min": 0, - "unit": "celsius" - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "blue", - "index": 0, - "value": null - }, - { - "color": "green", - "index": 1, - "value": 20 - }, - { - "color": "orange", - "index": 2, - "value": 40 - }, - { - "color": "red", - "index": 3, - "value": 80 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" ], + "fields": "", "values": false }, - "orientation": "horizontal" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { "alias": "Inside", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "H", "scenarioId": "csv_metric_values", "stringInput": "100,100,100" }, { "alias": "Outhouse", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "F", "scenarioId": "random_walk" } ], - "timeFrom": null, - "timeShift": null, "title": "Title above bar", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 20 + }, + { + "color": "orange", + "index": 2, + "value": 40 + }, + { + "color": "red", + "index": 3, + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, "gridPos": { "h": 7, - "w": 5, - "x": 6, + "w": 8, + "x": 9, "y": 0 }, "id": 12, "links": [], "options": { "displayMode": "gradient", - "fieldOptions": { - "calcs": ["mean"], - "defaults": { - "decimals": null, - "max": 100, - "min": 0, - "unit": "celsius" - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "blue", - "index": 0, - "value": null - }, - { - "color": "green", - "index": 1, - "value": 20 - }, - { - "color": "orange", - "index": 2, - "value": 40 - }, - { - "color": "red", - "index": 3, - "value": 80 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" ], + "fields": "", "values": false }, - "orientation": "horizontal" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { "alias": "Inside", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "H", "scenarioId": "csv_metric_values", "stringInput": "100,100,100" }, { "alias": "Outhouse", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "F", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "B", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "C", "scenarioId": "random_walk" } ], - "timeFrom": null, - "timeShift": null, "title": "Title to left of bar", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 20 + }, + { + "color": "orange", + "index": 2, + "value": 40 + }, + { + "color": "red", + "index": 3, + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, "gridPos": { "h": 7, "w": 7, - "x": 11, + "x": 17, "y": 0 }, - "id": 13, + "id": 23, "links": [], "options": { - "displayMode": "basic", - "fieldOptions": { - "calcs": ["mean"], - "defaults": { - "decimals": null, - "max": 100, - "min": 0, - "unit": "celsius" - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "blue", - "index": 0, - "value": null - }, - { - "color": "green", - "index": 1, - "value": 20 - }, - { - "color": "orange", - "index": 2, - "value": 40 - }, - { - "color": "red", - "index": 3, - "value": 80 - } + "displayMode": "gradient", + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" ], + "fields": "", "values": false }, - "orientation": "horizontal" + "showUnfilled": true, + "valueMode": "hidden" }, - "pluginVersion": "6.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { "alias": "Inside", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "H", "scenarioId": "csv_metric_values", "stringInput": "100,100,100" }, { "alias": "Outhouse", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "F", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "B", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "C", "scenarioId": "random_walk" } ], - "timeFrom": null, - "timeShift": null, - "title": "Basic mode", + "title": "Gradient hidden value", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 20 + }, + { + "color": "orange", + "index": 2, + "value": 40 + }, + { + "color": "red", + "index": 3, + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, "gridPos": { "h": 7, - "w": 6, - "x": 18, - "y": 0 + "w": 9, + "x": 0, + "y": 7 + }, + "id": 21, + "links": [], + "options": { + "displayMode": "basic", + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "valueMode": "text" + }, + "pluginVersion": "9.4.0-pre", + "targets": [ + { + "alias": "Inside", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "H", + "scenarioId": "csv_metric_values", + "stringInput": "100,100,100" + }, + { + "alias": "Outhouse", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "F", + "scenarioId": "random_walk" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "B", + "scenarioId": "random_walk" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "C", + "scenarioId": "random_walk" + } + ], + "title": "Basic mode + text color", + "type": "bargauge" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 20 + }, + { + "color": "orange", + "index": 2, + "value": 40 + }, + { + "color": "red", + "index": 3, + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 9, + "y": 7 + }, + "id": 13, + "links": [], + "options": { + "displayMode": "gradient", + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "valueMode": "text" + }, + "pluginVersion": "9.4.0-pre", + "targets": [ + { + "alias": "Inside", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "H", + "scenarioId": "csv_metric_values", + "stringInput": "100,100,100" + }, + { + "alias": "Outhouse", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "F", + "scenarioId": "random_walk" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "B", + "scenarioId": "random_walk" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "C", + "scenarioId": "random_walk" + } + ], + "title": "gradient + text color", + "type": "bargauge" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 20 + }, + { + "color": "orange", + "index": 2, + "value": 40 + }, + { + "color": "red", + "index": 3, + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 7, + "x": 17, + "y": 7 }, "id": 14, "links": [], "options": { "displayMode": "lcd", - "fieldOptions": { - "calcs": ["mean"], - "defaults": { - "decimals": null, - "max": 100, - "min": 0, - "unit": "celsius" - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "blue", - "index": 0, - "value": null - }, - { - "color": "green", - "index": 1, - "value": 20 - }, - { - "color": "orange", - "index": 2, - "value": 40 - }, - { - "color": "red", - "index": 3, - "value": 80 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" ], + "fields": "", "values": false }, - "orientation": "horizontal" + "showUnfilled": true, + "valueMode": "hidden" }, - "pluginVersion": "6.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { "alias": "Inside", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "H", "scenarioId": "csv_metric_values", "stringInput": "100,100,100" }, { "alias": "Outhouse", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "F", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "B", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "C", "scenarioId": "random_walk" } ], - "timeFrom": null, - "timeShift": null, - "title": "LED", + "title": "LED + hidden value", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "index": 0 + }, + { + "color": "orange", + "index": 1, + "value": 40 + }, + { + "color": "red", + "index": 2, + "value": 80 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, "gridPos": { "h": 9, "w": 11, "x": 0, - "y": 7 + "y": 14 }, "id": 7, "links": [], "options": { "displayMode": "lcd", - "fieldOptions": { - "calcs": ["mean"], - "defaults": { - "decimals": null, - "max": 100, - "min": 0, - "unit": "watt" - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "green", - "index": 0, - "value": null - }, - { - "color": "orange", - "index": 1, - "value": 40 - }, - { - "color": "red", - "index": 2, - "value": 80 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "mean" ], + "fields": "", "values": false }, - "orientation": "vertical" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "E", "scenarioId": "csv_metric_values", "stringInput": "10003,33333" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "F", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "G", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "H", "scenarioId": "csv_metric_values", "stringInput": "100,100,100" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "I", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "J", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "K", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "L", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "M", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "N", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "O", "scenarioId": "random_walk" - }, - { - "refId": "P", - "scenarioId": "random_walk" - }, - { - "refId": "Q", - "scenarioId": "random_walk" } ], - "timeFrom": null, - "timeShift": null, "title": "LED Vertical", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "index": 0 + }, + { + "color": "purple", + "index": 1, + "value": 50 + }, + { + "color": "blue", + "index": 2, + "value": 70 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, "gridPos": { "h": 9, "w": 13, "x": 11, - "y": 7 + "y": 14 }, "id": 8, "links": [], "options": { "displayMode": "basic", - "fieldOptions": { - "calcs": ["mean"], - "defaults": { - "decimals": null, - "max": 100, - "min": 0, - "unit": "watt" - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "green", - "index": 0, - "value": null - }, - { - "color": "purple", - "index": 1, - "value": 50 - }, - { - "color": "blue", - "index": 2, - "value": 70 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "mean" ], + "fields": "", "values": false }, - "orientation": "vertical" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "H", "scenarioId": "csv_metric_values", "stringInput": "100,100,100" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "B", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "C", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "D", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "I", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "J", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "K", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "L", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "M", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "N", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "O", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "P", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "Q", "scenarioId": "random_walk" } ], - "timeFrom": null, - "timeShift": null, "title": "Basic vertical ", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "index": 0 + }, + { + "color": "blue", + "index": 1, + "value": 40 + }, + { + "color": "red", + "index": 2, + "value": 80 + } + ] + } + }, + "overrides": [] + }, "gridPos": { "h": 7, "w": 11, "x": 0, - "y": 16 + "y": 23 }, "id": 16, "links": [], "options": { "displayMode": "lcd", - "fieldOptions": { - "calcs": ["last"], - "defaults": { - "max": 100, - "min": 0 - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "green", - "index": 0, - "value": null - }, - { - "color": "blue", - "index": 1, - "value": 40 - }, - { - "color": "red", - "index": 2, - "value": 80 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "last" ], + "fields": "", "values": false }, - "orientation": "horizontal" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.3.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "csv_metric_values", "stringInput": "1,20,90,30,5,0,-100" } ], - "timeFrom": null, - "timeShift": null, "title": "Negative value below min", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "index": 0 + }, + { + "color": "blue", + "index": 1, + "value": 40 + }, + { + "color": "red", + "index": 2, + "value": 80 + } + ] + } + }, + "overrides": [] + }, "gridPos": { "h": 7, "w": 3, "x": 11, - "y": 16 + "y": 23 }, "id": 17, "links": [], "options": { "displayMode": "lcd", - "fieldOptions": { - "calcs": ["last"], - "defaults": { - "max": 100, - "min": 0 - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "green", - "index": 0, - "value": null - }, - { - "color": "blue", - "index": 1, - "value": 40 - }, - { - "color": "red", - "index": 2, - "value": 80 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "last" ], + "fields": "", "values": false }, - "orientation": "vertical" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.3.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "csv_metric_values", "stringInput": "1,20,90,30,5,0,-100" } ], - "timeFrom": null, - "timeShift": null, "title": "Negative value below min", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": -10, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "index": 0 + }, + { + "color": "blue", + "index": 1, + "value": 40 + }, + { + "color": "red", + "index": 2, + "value": 80 + } + ] + } + }, + "overrides": [] + }, "gridPos": { "h": 7, "w": 3, "x": 14, - "y": 16 + "y": 23 }, "id": 18, "links": [], "options": { "displayMode": "lcd", - "fieldOptions": { - "calcs": ["last"], - "defaults": { - "max": 100, - "min": -10 - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "green", - "index": 0, - "value": null - }, - { - "color": "blue", - "index": 1, - "value": 40 - }, - { - "color": "red", - "index": 2, - "value": 80 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "last" ], + "fields": "", "values": false }, - "orientation": "vertical" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.3.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "csv_metric_values", "stringInput": "1,20,90,30,5,6" } ], - "timeFrom": null, - "timeShift": null, "title": "Positive value above min", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 35, + "min": -20, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 5 + }, + { + "color": "#EAB839", + "index": 2, + "value": 25 + }, + { + "color": "red", + "index": 3, + "value": 30 + } + ] + } + }, + "overrides": [] + }, "gridPos": { "h": 7, "w": 3, "x": 17, - "y": 16 + "y": 23 }, "id": 19, "links": [], "options": { "displayMode": "lcd", - "fieldOptions": { - "calcs": ["last"], - "defaults": { - "max": 35, - "min": -20 - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "blue", - "index": 0, - "value": null - }, - { - "color": "green", - "index": 1, - "value": 5 - }, - { - "color": "#EAB839", - "index": 2, - "value": 25 - }, - { - "color": "red", - "index": 3, - "value": 30 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "last" ], + "fields": "", "values": false }, - "orientation": "vertical" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.3.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "csv_metric_values", "stringInput": "1,20,90,30,5,6" } ], - "timeFrom": null, - "timeShift": null, "title": "Negative min ", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 35, + "min": -20, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 5 + }, + { + "color": "#EAB839", + "index": 2, + "value": 25 + }, + { + "color": "red", + "index": 3, + "value": 30 + } + ] + } + }, + "overrides": [] + }, "gridPos": { "h": 7, "w": 4, "x": 20, - "y": 16 + "y": 23 }, "id": 20, "links": [], "options": { "displayMode": "gradient", - "fieldOptions": { - "calcs": ["last"], - "defaults": { - "max": 35, - "min": -20 - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "blue", - "index": 0, - "value": null - }, - { - "color": "green", - "index": 1, - "value": 5 - }, - { - "color": "#EAB839", - "index": 2, - "value": 25 - }, - { - "color": "red", - "index": 3, - "value": 30 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "last" ], + "fields": "", "values": false }, - "orientation": "vertical" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.3.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "csv_metric_values", "stringInput": "30,30" } ], - "timeFrom": null, - "timeShift": null, "title": "Negative min", "type": "bargauge" } ], - "schemaVersion": 18, + "revision": 1, + "schemaVersion": 38, "style": "dark", - "tags": ["gdev", "panel-tests"], + "tags": [ + "gdev", + "panel-tests" + ], "templating": { "list": [] }, @@ -819,11 +1413,33 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] }, "timezone": "", "title": "Panel Tests - Bar Gauge", "uid": "O6f11TZWk", - "version": 12 + "version": 1, + "weekStart": "" } diff --git a/devenv/dev-dashboards/panel-table/table_tests_new.json b/devenv/dev-dashboards/panel-table/table_tests_new.json index a373c5080b3..92143922121 100644 --- a/devenv/dev-dashboards/panel-table/table_tests_new.json +++ b/devenv/dev-dashboards/panel-table/table_tests_new.json @@ -1,1100 +1,1043 @@ { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "links": [], - "liveNow": false, - "panels": [ - { - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 7, - "targets": [ - { - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "A" - } - ], - "title": "Cell styles", - "type": "row" + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 7, + "targets": [ { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "center", - "cellOptions": { - "mode": "gradient", - "type": "color-background" - }, - "filterable": false, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "green" - }, - { - "color": "blue", - "value": 20 - }, - { - "color": "orange", - "value": 60 - }, - { - "color": "red", - "value": 70 - } - ] - }, - "unit": "degree" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Max" - }, - "properties": [ - { - "id": "custom.width", - "value": 84 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Last" - }, - "properties": [ - { - "id": "custom.width", - "value": 78 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Mean" - }, - "properties": [ - { - "id": "custom.width", - "value": 74 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Field" - }, - "properties": [ - { - "id": "custom.align", - "value": "left" - } - ] - } - ] - }, - "gridPos": { - "h": 16, - "w": 7, - "x": 0, - "y": 1 - }, - "id": 4, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true, - "showRowNums": false, - "sortBy": [ - { - "desc": true, - "displayName": "Last" - } - ] - }, - "pluginVersion": "9.5.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 15, - "stringInput": "" - } - ], - "title": "Colored background", - "transformations": [ - { - "id": "reduce", - "options": { - "reducers": [ - "max", - "mean", - "last" - ] - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "filterable": false, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "orange" - }, - { - "color": "red", - "value": 50 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "A" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "mode": "gradient", - "type": "gauge" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Info" - }, - "properties": [ - { - "id": "custom.width", - "value": 92 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Min" - }, - "properties": [ - { - "id": "custom.width", - "value": 76 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Max" - }, - "properties": [ - { - "id": "custom.width", - "value": 89 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "custom.width", - "value": 165 - } - ] - } - ] - }, - "gridPos": { - "h": 16, - "w": 8, - "x": 7, - "y": 1 - }, - "id": 2, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true, - "showRowNums": false, - "sortBy": [ - { - "desc": false, - "displayName": "Min" - } - ] - }, - "pluginVersion": "9.5.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "refId": "A", - "scenarioId": "random_walk_table", - "stringInput": "" - } - ], - "title": "Bar gauge cells", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Time": true - }, - "indexByName": { - "Info": 1, - "Max": 3, - "Min": 2, - "Time": 0, - "Value": 4 - }, - "renameByName": {} - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "filterable": false, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "blue" - }, - { - "color": "green", - "value": 50 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "A" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "mode": "lcd", - "type": "gauge" - } - }, - { - "id": "custom.align", - "value": "center" - } - ] - } - ] - }, - "gridPos": { - "h": 16, - "w": 9, - "x": 15, - "y": 1 - }, - "id": 5, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true, - "showRowNums": false, - "sortBy": [] - }, - "pluginVersion": "9.5.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "refId": "A", - "scenarioId": "random_walk_table", - "stringInput": "" - } - ], - "title": "Retro LCD cell", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Info": false, - "Max": true, - "Min": true, - "Time": false - }, - "indexByName": { - "Info": 1, - "Max": 3, - "Min": 2, - "Time": 0, - "Value": 4 - }, - "renameByName": {} - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "rate" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "type": "sparkline" - } - }, - { - "id": "color", - "value": { - "mode": "continuous-GrYlRd" - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 9, - "x": 0, - "y": 17 - }, - "id": 14, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true, - "showRowNums": false - }, - "pluginVersion": "9.5.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "labels": "cluster=eu,service=checkout", - "min": 0.1, - "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 3 - } - ], - "title": "Sparkline cell", - "transformations": [ - { - "id": "timeSeriesTable", - "options": {} - }, - { - "id": "organize", - "options": { - "excludeByName": {}, - "indexByName": {}, - "renameByName": { - "Trend": "rate" - } - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "rate" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "drawStyle": "bars", - "fillOpacity": 40, - "gradientMode": "opacity", - "lineWidth": 1, - "showPoints": "auto", - "type": "sparkline" - } - }, - { - "id": "color", - "value": { - "mode": "continuous-GrYlRd" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "latency" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "fillOpacity": 40, - "gradientMode": "hue", - "type": "sparkline" - } - }, - { - "id": "color", - "value": { - "fixedColor": "orange", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 9, - "x": 9, - "y": 17 - }, - "id": 15, - "maxDataPoints": 100, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true, - "showRowNums": false - }, - "pluginVersion": "9.5.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "labels": "cluster=eu,service=checkout", - "min": 0.1, - "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 3 - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "hide": false, - "labels": "cluster=eu,service=checkout", - "min": 0.1, - "refId": "B", - "scenarioId": "random_walk", - "seriesCount": 3 - } - ], - "title": "Multiple sparkline cells per row", - "transformations": [ - { - "id": "timeSeriesTable", - "options": {} - }, - { - "id": "joinByField", - "options": { - "byField": "service", - "mode": "outer" - } - }, - { - "id": "organize", - "options": { - "excludeByName": { - "cluster 2": true - }, - "indexByName": {}, - "renameByName": { - "Trend": "rate", - "Trend #A": "rate", - "Trend #B": "latency", - "cluster 1": "cluster" - } - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 25 - }, - "id": 9, - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "refId": "A" - } - ], - "title": "Data links", - "type": "row" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "center", - "cellOptions": { - "type": "color-text" - }, - "filterable": false, - "inspect": false - }, - "decimals": 2, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "green" - }, - { - "color": "blue", - "value": 20 - }, - { - "color": "orange", - "value": 50 - }, - { - "color": "red", - "value": 70 - } - ] - }, - "unit": "percent" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "custom.align" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "{name=\"S1\", server=\"A\"}" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Details", - "url": "http://detail?serverLabel=${__field.labels.server}&valueNumeric=${__value.numeric}" - } - ] - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 26 - }, - "id": 3, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true, - "showRowNums": false - }, - "pluginVersion": "9.5.0-pre", - "targets": [ - { - "alias": "S1", - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "labels": "server=A", - "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 1, - "stringInput": "" - }, - { - "alias": "S2", - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "labels": "server=B", - "refId": "B", - "scenarioId": "random_walk", - "seriesCount": 1, - "stringInput": "" - }, - { - "alias": "S3", - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "labels": "server=C", - "refId": "C", - "scenarioId": "random_walk", - "seriesCount": 1, - "stringInput": "" - } - ], - "title": "Data link with labels and numeric value", - "transformations": [ - { - "id": "seriesToColumns", - "options": {} - } - ], - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "center", - "cellOptions": { - "type": "auto" - }, - "filterable": false, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "green" - }, - { - "color": "blue", - "value": 20 - }, - { - "color": "orange", - "value": 60 - }, - { - "color": "red", - "value": 70 - } - ] - }, - "unit": "degree" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 5, - "x": 12, - "y": 26 - }, - "id": 10, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": false, - "showRowNums": false, - "sortBy": [ - { - "desc": true, - "displayName": "Last" - } - ] - }, - "pluginVersion": "9.5.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "refId": "A", - "scenarioId": "random_walk_table", - "seriesCount": 5, - "stringInput": "" - } - ], - "title": "No header", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Min": true, - "Time": true, - "Value": true - }, - "indexByName": { - "Info": 2, - "Max": 4, - "Min": 3, - "Time": 0, - "Value": 1 - }, - "renameByName": {} - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 34 - }, - "id": 12, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": true - }, - "showHeader": true - }, - "pluginVersion": "9.4.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "refId": "A" - } - ], - "title": "Footer", - "type": "table" + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" } - ], - "refresh": "", - "revision": 1, - "schemaVersion": 38, - "style": "dark", - "tags": [ - "gdev", - "panel-tests" - ], - "templating": { - "list": [] + ], + "title": "Cell styles", + "type": "row" }, - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "mode": "gradient", + "type": "color-background" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "blue", + "value": 20 + }, + { + "color": "orange", + "value": 60 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "degree" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Max" + }, + "properties": [ + { + "id": "custom.width", + "value": 84 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Last" + }, + "properties": [ + { + "id": "custom.width", + "value": 78 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mean" + }, + "properties": [ + { + "id": "custom.width", + "value": 74 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Field" + }, + "properties": [ + { + "id": "custom.align", + "value": "left" + } + ] + } ] + }, + "gridPos": { + "h": 16, + "w": 7, + "x": 0, + "y": 1 + }, + "id": 4, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false, + "sortBy": [ + { + "desc": true, + "displayName": "Last" + } + ] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 15, + "stringInput": "" + } + ], + "title": "Colored background", + "transformations": [ + { + "id": "reduce", + "options": { + "reducers": [ + "max", + "mean", + "last" + ] + } + } + ], + "type": "table" }, - "timezone": "", - "title": "Panel Tests - React Table", - "uid": "U_bZIMRMk", - "version": 33, - "weekStart": "" + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "orange" + }, + { + "color": "red", + "value": 50 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Basic " + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "basic", + "type": "gauge" + } + }, + { + "id": "custom.inspect", + "value": false + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Gradient text color value" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge", + "valueMode": "text" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "LCD hidden value" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "lcd", + "type": "gauge", + "valueMode": "hidden" + } + } + ] + } + ] + }, + "gridPos": { + "h": 16, + "w": 17, + "x": 7, + "y": 1 + }, + "id": 2, + "options": { + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false, + "sortBy": [ + { + "desc": false, + "displayName": "Min" + } + ] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk_table", + "stringInput": "" + } + ], + "title": "Bar gauge cells", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Info": true, + "Time": true + }, + "indexByName": { + "Info": 1, + "Max": 3, + "Min": 2, + "Time": 0, + "Value": 4 + }, + "renameByName": { + "A": "LCD hidden value", + "Max": "Gradient text color value", + "Min": "Basic " + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "rate" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "fillOpacity": 29, + "gradientMode": "opacity", + "lineWidth": 1, + "type": "sparkline" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "cluster" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "service" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 9, + "x": 0, + "y": 17 + }, + "id": 14, + "maxDataPoints": 50, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false, + "sortBy": [] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "cluster=eu,service=checkout", + "min": 0.1, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 3 + } + ], + "title": "Sparkline cell", + "transformations": [ + { + "id": "timeSeriesTable", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "indexByName": {}, + "renameByName": { + "Trend": "rate" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "rate" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "hue", + "lineWidth": 0, + "showPoints": "auto", + "type": "sparkline" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + }, + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "latency" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "fillOpacity": 40, + "gradientMode": "opacity", + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "type": "sparkline" + } + }, + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "service" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "cluster" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 15, + "x": 9, + "y": 17 + }, + "id": 15, + "maxDataPoints": 30, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "cluster=eu,service=checkout", + "min": 0.1, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 3 + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "hide": false, + "labels": "cluster=eu,service=checkout", + "min": 0.1, + "refId": "B", + "scenarioId": "random_walk", + "seriesCount": 3 + } + ], + "title": "Multiple sparkline cells per row", + "transformations": [ + { + "id": "timeSeriesTable", + "options": {} + }, + { + "id": "joinByField", + "options": { + "byField": "service", + "mode": "outer" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "cluster 2": true + }, + "indexByName": {}, + "renameByName": { + "Trend": "rate", + "Trend #A": "rate", + "Trend #B": "latency", + "cluster 1": "cluster" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 9, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A" + } + ], + "title": "Data links", + "type": "row" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "color-text" + }, + "filterable": false, + "inspect": false + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "blue", + "value": 20 + }, + { + "color": "orange", + "value": 50 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "percent" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.align" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "{name=\"S1\", server=\"A\"}" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "title": "Details", + "url": "http://detail?serverLabel=${__field.labels.server}&valueNumeric=${__value.numeric}" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 3, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "alias": "S1", + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "server=A", + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 1, + "stringInput": "" + }, + { + "alias": "S2", + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "server=B", + "refId": "B", + "scenarioId": "random_walk", + "seriesCount": 1, + "stringInput": "" + }, + { + "alias": "S3", + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "server=C", + "refId": "C", + "scenarioId": "random_walk", + "seriesCount": 1, + "stringInput": "" + } + ], + "title": "Data link with labels and numeric value", + "transformations": [ + { + "id": "seriesToColumns", + "options": {} + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "blue", + "value": 20 + }, + { + "color": "orange", + "value": 60 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "degree" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 5, + "x": 12, + "y": 25 + }, + "id": 10, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": false, + "showRowNums": false, + "sortBy": [ + { + "desc": true, + "displayName": "Last" + } + ] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A", + "scenarioId": "random_walk_table", + "seriesCount": 5, + "stringInput": "" + } + ], + "title": "No header", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Min": true, + "Time": true, + "Value": true + }, + "indexByName": { + "Info": 2, + "Max": 4, + "Min": 3, + "Time": 0, + "Value": 1 + }, + "renameByName": {} + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 33 + }, + "id": 12, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A" + } + ], + "title": "Footer", + "type": "table" + } + ], + "refresh": "", + "revision": 1, + "schemaVersion": 38, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Panel Tests - React Table", + "uid": "U_bZIMRMk", + "version": 7, + "weekStart": "" } \ No newline at end of file diff --git a/docs/sources/developers/kinds/composable/bargaugepanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/bargaugepanelcfg/schema-reference.md index 1e8bf65de88..a4ecf690870 100644 --- a/docs/sources/developers/kinds/composable/bargaugepanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/bargaugepanelcfg/schema-reference.md @@ -27,6 +27,7 @@ It extends [SingleStatBaseOptions](#singlestatbaseoptions). | `minVizHeight` | uint32 | **Yes** | Default: `10`. | | `minVizWidth` | uint32 | **Yes** | Default: `0`. | | `showUnfilled` | boolean | **Yes** | Default: `true`. | +| `valueMode` | string | **Yes** | Allows for the table cell gauge display type to set the gauge mode.
Possible values are: `color`, `text`, `hidden`. | | `orientation` | string | No | *(Inherited from [SingleStatBaseOptions](#singlestatbaseoptions))*
TODO docs
Possible values are: `auto`, `vertical`, `horizontal`. | | `reduceOptions` | [ReduceDataOptions](#reducedataoptions) | No | *(Inherited from [SingleStatBaseOptions](#singlestatbaseoptions))*
TODO docs | | `text` | [VizTextDisplayOptions](#viztextdisplayoptions) | No | *(Inherited from [SingleStatBaseOptions](#singlestatbaseoptions))*
TODO docs | diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md index 2733eb423c0..92f5834fd29 100644 --- a/docs/sources/developers/kinds/core/dashboard/schema-reference.md +++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md @@ -195,11 +195,12 @@ TODO docs TODO docs -| Property | Type | Required | Description | -|----------|--------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------| -| `color` | string | **Yes** | TODO docs | -| `state` | string | No | TODO docs
TODO are the values here enumerable into a disjunction?
Some seem to be listed in typescript comment | -| `value` | number | No | TODO docs
FIXME the corresponding typescript field is required/non-optional, but nulls currently appear here when serializing -Infinity to JSON | +| Property | Type | Required | Description | +|----------|---------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| `color` | string | **Yes** | TODO docs | +| `index` | integer | No | Threshold index, an old property that is not needed an should only appear in older dashboards | +| `state` | string | No | TODO docs
TODO are the values here enumerable into a disjunction?
Some seem to be listed in typescript comment | +| `value` | number | No | TODO docs
FIXME the corresponding typescript field is required/non-optional, but nulls currently appear here when serializing -Infinity to JSON | ### ValueMapping diff --git a/docs/sources/panels-visualizations/visualizations/table/index.md b/docs/sources/panels-visualizations/visualizations/table/index.md index 089d7d3ffe4..c4b22966547 100644 --- a/docs/sources/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/panels-visualizations/visualizations/table/index.md @@ -108,6 +108,22 @@ The gauge is split up in small cells that are lit or unlit. {{< figure src="/static/img/docs/tables/lcd-gauge.png" max-width="500px" caption="LCD gauge" class="docs-image--no-shadow" >}} +#### Label Options + +Additionally, labels displayed alongside of the gauges can be set to be colored by value, match the theme text color, or be hidden. + +**Value Color** + +{{< figure src="/static/img/docs/tables/value-color-mode.png" max-width="500px" caption="Color Label by Value" class="docs-image--no-shadow" >}} + +**Text Color** + +{{< figure src="/static/img/docs/tables/text-color-mode.png" max-width="500px" caption="Color Label by theme color" class="docs-image--no-shadow" >}} + +**Hidden** + +{{< figure src="/static/img/docs/tables/hidden-mode.png" max-width="500px" caption="Hide Label" class="docs-image--no-shadow" >}} + ### JSON view Shows value formatted as code. If a value is an object the JSON view allowing browsing the JSON object will appear on hover. diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index f399d9a0d38..72d44fa1798 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -25,8 +25,8 @@ lineage: seqs: [ // Description of dashboard. description?: string // This property should only be used in dashboards defined by plugins. It is a quick check - // to see if the version has changed since the last time. Unclear why using the version property - // is insufficient. + // to see if the version has changed since the last time. Unclear why using the version property + // is insufficient. revision?: int64 @grafanamaturity(NeedsExpertReview) // For dashboards imported from the https://grafana.com/grafana/dashboards/ portal gnetId?: string @grafanamaturity(NeedsExpertReview) @@ -220,6 +220,8 @@ lineage: seqs: [ value?: number @grafanamaturity(NeedsExpertReview) // TODO docs color: string @grafanamaturity(NeedsExpertReview) + // Threshold index, an old property that is not needed an should only appear in older dashboards + index?: int32 @grafanamaturity(NeedsExpertReview) // TODO docs // TODO are the values here enumerable into a disjunction? // Some seem to be listed in typescript comment diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index f19c9ddbb2f..04b419b58e0 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -593,6 +593,15 @@ export enum BarGaugeDisplayMode { Lcd = 'lcd', } +/** + * Allows for the table cell gauge display type to set the gauge mode. + */ +export enum BarGaugeValueMode { + Color = 'color', + Hidden = 'hidden', + Text = 'text', +} + /** * TODO docs */ @@ -697,6 +706,7 @@ export interface TableImageCellOptions { export interface TableBarGaugeCellOptions { mode?: BarGaugeDisplayMode; type: TableCellDisplayMode.Gauge; + valueDisplayMode?: BarGaugeValueMode; } /** diff --git a/packages/grafana-schema/src/common/mudball.cue b/packages/grafana-schema/src/common/mudball.cue index 0981e652ffd..9286e5c5783 100644 --- a/packages/grafana-schema/src/common/mudball.cue +++ b/packages/grafana-schema/src/common/mudball.cue @@ -242,6 +242,9 @@ VizLegendOptions: { // for the bar gauge component of Grafana UI BarGaugeDisplayMode: "basic" | "lcd" | "gradient" @cuetsy(kind="enum") +// Allows for the table cell gauge display type to set the gauge mode. +BarGaugeValueMode: "color" | "text" | "hidden" @cuetsy(kind="enum") + // TODO docs VizTooltipOptions: { mode: TooltipDisplayMode diff --git a/packages/grafana-schema/src/common/table.cue b/packages/grafana-schema/src/common/table.cue index 84c819b5511..1522e61ed87 100644 --- a/packages/grafana-schema/src/common/table.cue +++ b/packages/grafana-schema/src/common/table.cue @@ -52,6 +52,7 @@ TableImageCellOptions: { TableBarGaugeCellOptions: { type: TableCellDisplayMode & "gauge" mode?: BarGaugeDisplayMode + valueDisplayMode?: BarGaugeValueMode } @cuetsy(kind="interface") // Sparkline cell options diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index 18e8c544351..b1332f1b72c 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -238,6 +238,10 @@ export interface Threshold { * TODO docs */ color: string; + /** + * Threshold index, an old property that is not needed an should only appear in older dashboards + */ + index?: number; /** * TODO docs * TODO are the values here enumerable into a disjunction? diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx index 0a8e36dec9d..b4bfb2ca3d4 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx @@ -11,18 +11,18 @@ import { getDisplayProcessor, createTheme, } from '@grafana/data'; -import { BarGaugeDisplayMode } from '@grafana/schema'; +import { BarGaugeDisplayMode, BarGaugeValueMode } from '@grafana/schema'; import { BarGauge, Props, - getCellColor, - getValueColor, + getTextValueColor, getBasicAndGradientStyles, getBarGradient, getTitleStyles, getValuePercent, calculateBarAndValueDimensions, + getCellColor, } from './BarGauge'; const green = '#73BF69'; @@ -63,7 +63,7 @@ function getProps(propOverrides?: Partial): Props { } function getValue(value: number, title?: string): DisplayValue { - return { numeric: value, text: value.toString(), title: title }; + return { numeric: value, text: value.toString(), title: title, color: '#FF0000' }; } describe('BarGauge', () => { @@ -134,12 +134,12 @@ describe('BarGauge', () => { it('should get the threshold color if value is same as a threshold', () => { const props = getProps(); props.value = props.display!(70); - expect(getValueColor(props)).toEqual(orange); + expect(getTextValueColor(props)).toEqual(orange); }); it('should get the base threshold', () => { const props = getProps(); props.value = props.display!(-10); - expect(getValueColor(props)).toEqual(green); + expect(getTextValueColor(props)).toEqual(green); }); }); @@ -325,5 +325,46 @@ describe('BarGauge', () => { ); expect(result.valueWidth).toBe(21); }); + + it('valueWidth be zero if valueMode is hideen', () => { + const result = calculateBarAndValueDimensions( + getProps({ + height: 30, + width: 100, + value: getValue(1, 'AA'), + orientation: VizOrientation.Horizontal, + valueDisplayMode: BarGaugeValueMode.Hidden, + }) + ); + expect(result.valueWidth).toBe(0); + }); + }); + + describe('With valueMode set to text color', () => { + it('should color value using text color', () => { + const props = getProps({ + width: 150, + value: getValue(100), + orientation: VizOrientation.Vertical, + valueDisplayMode: BarGaugeValueMode.Text, + }); + const styles = getBasicAndGradientStyles(props); + expect(styles.bar.background).toBe('rgba(255, 0, 0, 0.35)'); + expect(styles.value.color).toBe('rgb(204, 204, 220)'); + }); + }); + + describe('With valueMode set to text value', () => { + it('should color value value color', () => { + const props = getProps({ + width: 150, + value: getValue(100), + orientation: VizOrientation.Vertical, + valueDisplayMode: BarGaugeValueMode.Color, + }); + const styles = getBasicAndGradientStyles(props); + expect(styles.bar.background).toBe('rgba(255, 0, 0, 0.35)'); + expect(styles.value.color).toBe('#FF0000'); + }); }); }); diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index a38179b6417..47ab798514d 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -20,7 +20,7 @@ import { VizOrientation, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { BarGaugeDisplayMode, VizTextDisplayOptions } from '@grafana/schema'; +import { BarGaugeDisplayMode, BarGaugeValueMode, VizTextDisplayOptions } from '@grafana/schema'; import { Themeable2 } from '../../types'; import { calculateFontSize, measureText } from '../../utils/measureText'; @@ -49,6 +49,7 @@ export interface Props extends Themeable2 { className?: string; showUnfilled?: boolean; alignmentFactors?: DisplayValueAlignmentFactors; + valueDisplayMode?: BarGaugeValueMode; } export class BarGauge extends PureComponent { @@ -111,17 +112,19 @@ export class BarGauge extends PureComponent { } renderBasicAndGradientBars(): ReactNode { - const { value, showUnfilled } = this.props; + const { value, showUnfilled, valueDisplayMode } = this.props; const styles = getBasicAndGradientStyles(this.props); return (
- + {valueDisplayMode !== BarGaugeValueMode.Hidden && ( + + )} {showUnfilled &&
}
@@ -129,7 +132,8 @@ export class BarGauge extends PureComponent { } renderRetroBars(): ReactNode { - const { display, field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth, text } = this.props; + const { display, field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth, text, valueDisplayMode } = + this.props; const { valueHeight, valueWidth, maxBarHeight, maxBarWidth, wrapperWidth, wrapperHeight } = calculateBarAndValueDimensions(this.props); const minValue = field.min ?? GAUGE_DEFAULT_MINIMUM; @@ -141,7 +145,7 @@ export class BarGauge extends PureComponent { const cellSpacing = itemSpacing!; const cellCount = Math.floor(maxSize / lcdCellWidth!); const cellSize = Math.floor((maxSize - cellSpacing * cellCount) / cellCount); - const valueColor = getValueColor(this.props); + const valueColor = getTextValueColor(this.props); const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value; const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation, text); @@ -192,11 +196,13 @@ export class BarGauge extends PureComponent { return (
{cells} - + {valueDisplayMode !== BarGaugeValueMode.Hidden && ( + + )}
); } @@ -338,7 +344,7 @@ interface BarAndValueDimensions { * Only exported for unit tests **/ export function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions { - const { height, width, orientation, text, alignmentFactors } = props; + const { height, width, orientation, text, alignmentFactors, valueDisplayMode } = props; const titleDim = calculateTitleDimensions(props); const value = alignmentFactors ?? props.value; const valueString = formattedValueToString(value); @@ -363,13 +369,25 @@ export function calculateBarAndValueDimensions(props: Props): BarAndValueDimensi } valueWidth = width; + + if (valueDisplayMode === BarGaugeValueMode.Hidden) { + valueHeight = 0; + valueWidth = 0; + } + maxBarHeight = height - (titleDim.height + valueHeight); maxBarWidth = width; wrapperWidth = width; wrapperHeight = height - titleDim.height; } else { - valueHeight = height - titleDim.height; - valueWidth = Math.max(Math.min(width * 0.2, MAX_VALUE_WIDTH), realValueWidth); + // Calculate the width and the height of the given values + if (valueDisplayMode === BarGaugeValueMode.Hidden) { + valueHeight = 0; + valueWidth = 0; + } else { + valueHeight = height - titleDim.height; + valueWidth = Math.max(Math.min(width * 0.2, MAX_VALUE_WIDTH), realValueWidth); + } maxBarHeight = height - titleDim.height; maxBarWidth = width - valueWidth - titleDim.width; @@ -447,10 +465,11 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles const minValue = field.min ?? GAUGE_DEFAULT_MINIMUM; const maxValue = field.max ?? GAUGE_DEFAULT_MAXIMUM; const valuePercent = getValuePercent(value.numeric, minValue, maxValue); - const valueColor = getValueColor(props); + const textColor = getTextValueColor(props); + const barColor = value.color ?? FALLBACK_COLOR; const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value; - const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation, text); + const valueStyles = getValueStyles(valueToBaseSizeOn, textColor, valueWidth, valueHeight, orientation, text); const isBasic = displayMode === 'basic'; const wrapperStyles: CSSProperties = { @@ -491,8 +510,8 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles if (isBasic) { // Basic styles - barStyles.background = `${tinycolor(valueColor).setAlpha(0.35).toRgbString()}`; - barStyles.borderTop = `2px solid ${valueColor}`; + barStyles.background = `${tinycolor(barColor).setAlpha(0.35).toRgbString()}`; + barStyles.borderTop = `2px solid ${barColor}`; } else { // Gradient styles barStyles.background = getBarGradient(props, maxBarHeight); @@ -517,8 +536,8 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles if (isBasic) { // Basic styles - barStyles.background = `${tinycolor(valueColor).setAlpha(0.35).toRgbString()}`; - barStyles.borderRight = `2px solid ${valueColor}`; + barStyles.background = `${tinycolor(barColor).setAlpha(0.35).toRgbString()}`; + barStyles.borderRight = `2px solid ${barColor}`; } else { // Gradient styles barStyles.background = getBarGradient(props, maxBarWidth); @@ -598,7 +617,11 @@ export function getBarGradient(props: Props, maxSize: number): string { /** * Only exported to for unit test */ -export function getValueColor(props: Props): string { +export function getTextValueColor(props: Props): string { + if (props.valueDisplayMode === 'text') { + return props.theme.colors.text.primary; + } + const { value } = props; if (value.color) { return value.color; diff --git a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx index 3bbb3b2761a..f3d1f5ef9e2 100644 --- a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx @@ -2,7 +2,7 @@ import { isFunction } from 'lodash'; import React, { FC } from 'react'; import { ThresholdsConfig, ThresholdsMode, VizOrientation, getFieldConfigWithMinMax } from '@grafana/data'; -import { BarGaugeDisplayMode } from '@grafana/schema'; +import { BarGaugeDisplayMode, BarGaugeValueMode } from '@grafana/schema'; import { BarGauge } from '../BarGauge/BarGauge'; import { DataLinksContextMenu, DataLinksContextMenuApi } from '../DataLinks/DataLinksContextMenu'; @@ -26,6 +26,8 @@ const defaultScale: ThresholdsConfig = { export const BarGaugeCell: FC = (props) => { const { field, innerWidth, tableStyles, cell, cellProps, row } = props; + const displayValue = field.display!(cell.value); + const cellOptions = getCellOptions(field); let config = getFieldConfigWithMinMax(field, false); if (!config.thresholds) { @@ -35,14 +37,15 @@ export const BarGaugeCell: FC = (props) => { }; } - const displayValue = field.display!(cell.value); - - // Set default display mode + // Set default display mode and update if defined + // and update the valueMode if defined let barGaugeMode: BarGaugeDisplayMode = BarGaugeDisplayMode.Gradient; + let valueDisplayMode: BarGaugeValueMode | undefined = undefined; - const cellOptions = getCellOptions(field); if (cellOptions.type === TableCellDisplayMode.Gauge) { barGaugeMode = cellOptions.mode ?? BarGaugeDisplayMode.Gradient; + valueDisplayMode = + cellOptions.valueDisplayMode !== undefined ? cellOptions.valueDisplayMode : BarGaugeValueMode.Text; } const getLinks = () => { @@ -73,6 +76,7 @@ export const BarGaugeCell: FC = (props) => { itemSpacing={1} lcdCellWidth={8} displayMode={barGaugeMode} + valueDisplayMode={valueDisplayMode} /> ); }; @@ -84,21 +88,7 @@ export const BarGaugeCell: FC = (props) => { {(api) => renderComponent(api)} )} - {!hasLinks && ( - - )} + {!hasLinks && renderComponent({})}
); }; diff --git a/pkg/kinds/dashboard/dashboard_types_gen.go b/pkg/kinds/dashboard/dashboard_types_gen.go index 31fe5859db1..792535a1451 100644 --- a/pkg/kinds/dashboard/dashboard_types_gen.go +++ b/pkg/kinds/dashboard/dashboard_types_gen.go @@ -701,6 +701,9 @@ type Threshold struct { // TODO docs Color string `json:"color"` + // Threshold index, an old property that is not needed an should only appear in older dashboards + Index *int32 `json:"index,omitempty"` + // TODO docs // TODO are the values here enumerable into a disjunction? // Some seem to be listed in typescript comment diff --git a/pkg/kindsys/report.json b/pkg/kindsys/report.json index 0627ecec5fc..f900e8cd829 100644 --- a/pkg/kindsys/report.json +++ b/pkg/kindsys/report.json @@ -296,7 +296,7 @@ 0 ], "description": "A Grafana dashboard.", - "grafanaMaturityCount": 139, + "grafanaMaturityCount": 140, "lineageIsGroup": false, "links": { "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/dashboard/schema-reference", diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index cb01fc9b87b..99a5ed13567 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -12,12 +12,11 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/expr" "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/expr" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngstore "github.com/grafana/grafana/pkg/services/ngalert/store" diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.test.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.test.tsx index 3b74a48a67c..bd40346e0f2 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanel.test.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanel.test.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { dateMath, dateTime, EventBus, LoadingState, TimeRange, toDataFrame, VizOrientation } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { BarGaugeDisplayMode } from '@grafana/schema'; +import { BarGaugeDisplayMode, BarGaugeValueMode } from '@grafana/schema'; import { BarGaugePanel, BarGaugePanelProps } from './BarGaugePanel'; @@ -101,6 +101,7 @@ function buildPanelData(overrideValues?: Partial): BarGaugeP showUnfilled: true, minVizHeight: 10, minVizWidth: 0, + valueMode: BarGaugeValueMode.Color, }, transparent: false, timeRange, diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx index 0165cf3677f..326e5b3fb60 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx @@ -49,6 +49,7 @@ export class BarGaugePanel extends PureComponent { className={targetClassName} alignmentFactors={count > 1 ? alignmentFactors : undefined} showUnfilled={options.showUnfilled} + valueDisplayMode={options.valueMode} /> ); }; diff --git a/public/app/plugins/panel/bargauge/module.tsx b/public/app/plugins/panel/bargauge/module.tsx index bfe41e90853..778abfd49ab 100644 --- a/public/app/plugins/panel/bargauge/module.tsx +++ b/public/app/plugins/panel/bargauge/module.tsx @@ -1,5 +1,5 @@ import { PanelPlugin, VizOrientation } from '@grafana/data'; -import { BarGaugeDisplayMode } from '@grafana/schema'; +import { BarGaugeDisplayMode, BarGaugeValueMode } from '@grafana/schema'; import { commonOptionsBuilder, sharedSingleStatPanelChangedHandler } from '@grafana/ui'; import { addOrientationOption, addStandardDataReduceOptions } from '../stat/common'; @@ -29,6 +29,18 @@ export const plugin = new PanelPlugin(BarGaugePanel) }, defaultValue: defaultPanelOptions.displayMode, }) + .addRadio({ + path: 'valueMode', + name: 'Value display', + settings: { + options: [ + { value: BarGaugeValueMode.Color, label: 'Value color' }, + { value: BarGaugeValueMode.Text, label: 'Text color' }, + { value: BarGaugeValueMode.Hidden, label: 'Hidden' }, + ], + }, + defaultValue: defaultPanelOptions.valueMode, + }) .addBooleanSwitch({ path: 'showUnfilled', name: 'Show unfilled area', diff --git a/public/app/plugins/panel/bargauge/panelcfg.cue b/public/app/plugins/panel/bargauge/panelcfg.cue index 73f133fb39c..ffb83db0724 100644 --- a/public/app/plugins/panel/bargauge/panelcfg.cue +++ b/public/app/plugins/panel/bargauge/panelcfg.cue @@ -29,6 +29,7 @@ composableKinds: PanelCfg: { PanelOptions: { common.SingleStatBaseOptions displayMode: common.BarGaugeDisplayMode | *"gradient" + valueMode: common.BarGaugeValueMode | *"color" showUnfilled: bool | *true minVizWidth: uint32 | *0 minVizHeight: uint32 | *10 diff --git a/public/app/plugins/panel/bargauge/panelcfg.gen.ts b/public/app/plugins/panel/bargauge/panelcfg.gen.ts index f42c6814cfc..9d2b6b5135b 100644 --- a/public/app/plugins/panel/bargauge/panelcfg.gen.ts +++ b/public/app/plugins/panel/bargauge/panelcfg.gen.ts @@ -17,6 +17,7 @@ export interface PanelOptions extends common.SingleStatBaseOptions { minVizHeight: number; minVizWidth: number; showUnfilled: boolean; + valueMode: common.BarGaugeValueMode; } export const defaultPanelOptions: Partial = { @@ -24,4 +25,5 @@ export const defaultPanelOptions: Partial = { minVizHeight: 10, minVizWidth: 0, showUnfilled: true, + valueMode: common.BarGaugeValueMode.Color, }; diff --git a/public/app/plugins/panel/table/TableCellOptionEditor.tsx b/public/app/plugins/panel/table/TableCellOptionEditor.tsx index ba23e886ddd..8de4e823063 100644 --- a/public/app/plugins/panel/table/TableCellOptionEditor.tsx +++ b/public/app/plugins/panel/table/TableCellOptionEditor.tsx @@ -1,10 +1,11 @@ +import { css } from '@emotion/css'; import { merge } from 'lodash'; import React, { useState } from 'react'; -import { SelectableValue } from '@grafana/data'; +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { config } from '@grafana/runtime'; import { TableCellOptions } from '@grafana/schema'; -import { Field, Select, TableCellDisplayMode } from '@grafana/ui'; +import { Field, Select, TableCellDisplayMode, useStyles2 } from '@grafana/ui'; import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor'; import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor'; @@ -25,8 +26,8 @@ interface Props { export const TableCellOptionEditor = ({ value, onChange }: Props) => { const cellType = value.type; + const styles = useStyles2(getStyles); const currentMode = cellDisplayModeOptions.find((o) => o.value!.type === cellType)!; - let [settingCache, setSettingCache] = useState>({}); // Update display mode on change @@ -56,7 +57,7 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => { // Setup and inject editor return ( - <> +
+ + + + + + ); +} + const barGaugeOpts: SelectableValue[] = [ { value: BarGaugeDisplayMode.Basic, label: 'Basic' }, { value: BarGaugeDisplayMode.Gradient, label: 'Gradient' }, { value: BarGaugeDisplayMode.Lcd, label: 'Retro LCD' }, ]; -export const BarGaugeCellOptionsEditor = ({ - cellOptions, - onChange, -}: TableCellEditorProps) => { - // Set the display mode on change - const onCellOptionsChange = (v: SelectableValue) => { - cellOptions.mode = v.value; - onChange(cellOptions); - }; - - return ( - - - - - + +