diff --git a/package.json b/package.json index 6fc67abb..b3ba0be6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@netdata/charts", - "version": "4.4.8", + "version": "4.5.0", "description": "Netdata frontend SDK and chart utilities", "main": "dist/index.js", "module": "dist/es6/index.js", diff --git a/src/agentv2.stories.js b/src/agentv2.stories.js index 78c906b8..ba69a136 100644 --- a/src/agentv2.stories.js +++ b/src/agentv2.stories.js @@ -7,6 +7,7 @@ import NumberComponent from "@/components/number" import D3pieComponent from "@/components/d3pie" import BarsComponent from "@/components/bars" import GroupBoxes from "@/components/groupBoxes" +import Table from "@/components/table" import makeDefaultSDK from "./makeDefaultSDK" export const Chart = ({ nodesScope, contextScope, contexts, host, theme, singleDimension }) => { @@ -27,6 +28,25 @@ export const Chart = ({ nodesScope, contextScope, contexts, host, theme, singleD sdk.appendChild(chart) + const chart7 = sdk.makeChart({ + attributes: { + id: "control", + selectedContexts: [contexts], + nodesScope: [nodesScope], + contextScope: ["disk.io", "disk.ops", "disk.await", "disk.util"], + host: host, + aggregationMethod: "avg", + agent: true, + syncHover: true, + groupingMethod: "average", + chartLibrary: "table", + groupBy: ["label", "dimension", "context", "node"], + groupByLabel: ["device"], + }, + }) + + sdk.appendChild(chart7) + const chart2 = sdk.makeChart({ attributes: { selectedContexts: [contexts], @@ -171,6 +191,7 @@ export const Chart = ({ nodesScope, contextScope, contexts, host, theme, singleD + ) @@ -186,7 +207,7 @@ export default { contexts: "*", singleDimension: "*", theme: "default", - host: "http://10.10.10.20:19999/api/v2", + host: "http://10.20.4.200:19999/api/v2", }, argTypes: { host: { diff --git a/src/chartLibraries/table/index.js b/src/chartLibraries/table/index.js new file mode 100644 index 00000000..f6d7dfb7 --- /dev/null +++ b/src/chartLibraries/table/index.js @@ -0,0 +1,77 @@ +import makeChartUI from "@/sdk/makeChartUI" +import { unregister } from "@/helpers/makeListeners" +import makeResizeObserver from "@/helpers/makeResizeObserver" + +export default (sdk, chart) => { + const chartUI = makeChartUI(sdk, chart) + let listeners + let resizeObserver + let prevMin + let prevMax + + const mount = element => { + chartUI.mount(element) + + resizeObserver = makeResizeObserver( + element, + () => chartUI.trigger("resize"), + () => chartUI.trigger("resize") + ) + + const { loaded } = chart.getAttributes() + + listeners = unregister( + chart.onAttributeChange("hoverX", render), + !loaded && chart.onceAttributeChange("loaded", render) + ) + + render() + } + + const render = () => { + chartUI.render() + + const { hoverX, loaded } = chart.getAttributes() + + if (!loaded) return + + const { data } = chart.getPayload() + + const row = hoverX ? chart.getClosestRow(hoverX[0]) : data.length - 1 + + const rowData = data[row] + if (!Array.isArray(rowData)) return + + chartUI.render() + const min = chart.getAttribute("min") + const max = chart.getAttribute("max") + + if (min !== prevMin || max !== prevMax) { + chartUI.sdk.trigger("yAxisChange", chart, min, max) + } + + prevMin = min + prevMax = max + + chartUI.trigger("rendered") + } + + const unmount = () => { + if (listeners) listeners() + + if (resizeObserver) resizeObserver() + + chartUI.unmount() + prevMin = null + prevMax = null + } + + const instance = { + ...chartUI, + mount, + unmount, + render, + } + + return instance +} diff --git a/src/components/line/dimensions/color.js b/src/components/line/dimensions/color.js index 6923a01d..e24b3e5a 100644 --- a/src/components/line/dimensions/color.js +++ b/src/components/line/dimensions/color.js @@ -54,9 +54,9 @@ export const BaseColorBar = ({ ) } -export const ColorBar = ({ id, valueKey, ...rest }) => { +export const ColorBar = ({ id, partIndex, valueKey, ...rest }) => { const chart = useChart() - const bg = valueKey === "arp" ? "anomalyTextLite" : chart.selectDimensionColor(id) + const bg = valueKey === "arp" ? "anomalyTextLite" : chart.selectDimensionColor(id, partIndex) const min = valueKey === "arp" ? 0 : chart.getAttribute("min") const max = valueKey === "arp" ? 100 : chart.getAttribute("max") @@ -80,9 +80,9 @@ export const ColorBar = ({ id, valueKey, ...rest }) => { ) } -const ColorValue = ({ id, ...rest }) => { +const ColorValue = ({ id, partIndex, ...rest }) => { const chart = useChart() - const bg = chart.selectDimensionColor(id) + const bg = chart.selectDimensionColor(id, partIndex) if (!bg) return null diff --git a/src/components/line/dimensions/name.js b/src/components/line/dimensions/name.js index da74c92c..7769b532 100644 --- a/src/components/line/dimensions/name.js +++ b/src/components/line/dimensions/name.js @@ -17,9 +17,9 @@ export const Name = memo( )) ) -const Container = ({ id, ...rest }) => { +const Container = ({ id, partIndex, ...rest }) => { const chart = useChart() - const name = chart.getDimensionName(id) + const name = chart.getDimensionName(id, partIndex) return {name} } diff --git a/src/components/table/columns.js b/src/components/table/columns.js new file mode 100644 index 00000000..cc129dac --- /dev/null +++ b/src/components/table/columns.js @@ -0,0 +1,268 @@ +import React from "react" +import { Flex, TextSmall, TextMicro } from "@netdata/netdata-ui" +import styled from "styled-components" +import Color, { ColorBar } from "@/components/line/dimensions/color" +import Name from "@/components/line/dimensions/name" +import Units from "@/components/line/dimensions/units" +import Value, { Value as ValuePart } from "@/components/line/dimensions/value" +import { useChart, useAttributeValue, useVisibleDimensionId } from "@/components/provider" +import Label from "@/components/filterToolbox/label" +import { rowFlavours } from "@/components/line/popover/dimensions" + +const ColorBackground = styled(ColorBar).attrs({ + position: "absolute", + top: 1, + left: 2, + backgroundOpacity: 0.4, + round: 0.5, +})`` + +const rowValueKeys = { + ANOMALY_RATE: "arp", + default: "value", +} + +const metricsByValue = { + dimension: "dimensions", + node: "nodes", + instance: "instances", + label: "labels", + value: "values", + default: "values", +} + +const emptyArray = [] + +export const labelColumn = ({ fallbackExpandKey, partIndex, header = "Name" } = {}) => ({ + id: `label${header || ""}${partIndex || ""}`, + header: () => {header}, + size: 100, + minSize: 45, + cell: ({ + row: { + original: { ids }, + depth = 0, + getCanExpand, + getToggleExpandedHandler, + getIsExpanded, + }, + }) => { + const [, row] = useAttributeValue("hoverX") || emptyArray + const rowFlavour = rowFlavours[row] || rowFlavours.default + + const chart = useChart() + const visible = ids.some(chart.isDimensionVisible) + + const [firstId] = ids + + return ( + + + {visible && ( + + + + )} + + + {getCanExpand() && ( + + ) + }, +}) + +const ValueOnDot = ({ children, fractionDigits = 0, ...rest }) => { + const [first, last] = children.toString().split(".") + + return ( + + + {first} + + {typeof last !== "undefined" && .} + + {last} + + + ) +} + +export const valueColumn = ({ context = "Dimensions", dimension = "Value" }) => ({ + id: `value${context}${dimension}`, + header: () => { + return ( + + {dimension} + + ) + }, + size: 80, + minSize: 45, + cell: ({ + row: { + original: { key, ids, contextGroups }, + depth = 0, + getCanExpand, + getToggleExpandedHandler, + getIsExpanded, + }, + }) => { + const id = (contextGroups?.[context]?.[dimension] || ids).find(id => id.includes(key)) + const visible = useVisibleDimensionId(id) + + const chart = useChart() + const fractionDigits = chart.getAttribute("unitsConversionFractionDigits") + + return ( + + ) + }, + sortingFn: "basic", +}) + +// export const anomalyColumn = ({ period, objKey }) => ({ +// id: objKey ? `${objKey}-arp` : "arp", +// header: AR %, +// size: 45, +// minSize: 45, +// cell: ({ +// row: { original: id, depth = 0, getCanExpand, getToggleExpandedHandler, getIsExpanded }, +// }) => { +// const visible = useVisibleDimensionId(id) + +// return ( +// +// ) +// }, +// sortingFn: "basic", +// }) + +// export const minColumn = ({ period, objKey }) => ({ +// id: objKey ? `${objKey}-min` : "min", +// header: ( +// +// Min +// +// ), +// size: 45, +// minSize: 45, +// cell: ({ +// row: { original: id, depth = 0, getCanExpand, getToggleExpandedHandler, getIsExpanded }, +// }) => { +// const visible = useVisibleDimensionId(id) + +// return ( +// +// ) +// }, +// sortingFn: "basic", +// }) + +// export const avgColumn = ({ period, objKey }) => ({ +// id: objKey ? `${objKey}-avg` : "avg", +// header: ( +// +// Avg +// +// ), +// size: 45, +// minSize: 45, +// cell: ({ +// row: { original: id, depth = 0, getCanExpand, getToggleExpandedHandler, getIsExpanded }, +// }) => { +// const visible = useVisibleDimensionId(id) + +// return ( +// +// ) +// }, +// sortingFn: "basic", +// }) + +// export const maxColumn = ({ period, objKey }) => ({ +// id: objKey ? `${objKey}-max` : "max", +// header: ( +// +// Max +// +// ), +// size: 45, +// minSize: 45, +// cell: ({ +// row: { original: id, depth = 0, getCanExpand, getToggleExpandedHandler, getIsExpanded }, +// }) => { +// const visible = useVisibleDimensionId(id) + +// return ( +// +// ) +// }, +// sortingFn: "basic", +// }) diff --git a/src/components/table/index.js b/src/components/table/index.js new file mode 100644 index 00000000..355d8d14 --- /dev/null +++ b/src/components/table/index.js @@ -0,0 +1,169 @@ +import React, { forwardRef, useMemo } from "react" +import groupBy from "lodash/groupBy" +import isEmpty from "lodash/isEmpty" +import { Table, TextSmall } from "@netdata/netdata-ui" +import ChartContainer from "@/components/chartContainer" +import { useChart, useDimensionIds, useAttributeValue } from "@/components/provider" +import withChart from "@/components/hocs/withChart" +import { ChartWrapper } from "@/components/hocs/withTile" +import { uppercase } from "@/helpers/objectTransform" +import FontSizer from "@/components/helpers/fontSizer" +import { labelColumn, valueColumn, anomalyColumn, minColumn, avgColumn, maxColumn } from "./columns" + +const keepoutRegex = ".*" +const keepRegex = "(" + keepoutRegex + ")" + +const useColumns = (options = {}) => { + const { period, dimensionIds, groups, labels, rowGroups, contextGroups } = options + + const hover = useAttributeValue("hoverX") + + return useMemo(() => { + return [ + { + id: "Instance", + header: () => { + console.log(1) + return "Instance" + }, + columns: labels.map(label => + labelColumn({ header: uppercase(label), partIndex: groups.findIndex(gi => gi === label) }) + ), + }, + ...Object.keys(contextGroups).map(context => { + return { + id: `Context-${context}`, + header: () => { + console.log(2, context) + return context + }, + columns: Object.keys(contextGroups[context]).map(dimension => + valueColumn({ context, dimension, ...options }) + ), + } + }), + ] + }, [period, !!hover, dimensionIds]) +} + +const columnAttrs = ["context", "dimension"] + +const groupByColumn = (result, ids, groups, attrs = columnAttrs) => { + const [attr, ...restAttrs] = attrs + const groupByRegex = new RegExp( + groups.reduce((s, g) => { + s = s + (s ? "," : "") + + if (attr === g) { + s = s + keepRegex + } else { + s = s + keepoutRegex + } + + return s + }, "") + ) + + if (isEmpty(result)) { + result = groupBy(ids, value => { + const [, ...matches] = value.match(groupByRegex) + return matches.join(",") + }) + } else { + Object.keys(result).forEach(key => { + result[key] = groupBy(result[key], value => { + const [, ...matches] = value.match(groupByRegex) + return matches.join(",") + }) + }) + } + + if (!restAttrs.length) return result + + return groupByColumn(result, ids, groups, restAttrs) +} + +const Dimensions = () => { + const dimensionIds = useDimensionIds() + + const tab = useAttributeValue("weightsTab") + + const chart = useChart() + const groups = chart.getDimensionGroups() + const tableColumns = chart.getAttribute("tableColumns") + + const [rowGroups, contextGroups, labels] = useMemo(() => { + let forRows = [] + + let groupByRegex = new RegExp( + groups.reduce((s, g) => { + s = s + (s ? "," : "") + + if (tableColumns.includes(g)) { + s = s + keepoutRegex + } else { + s = s + keepRegex + forRows.push(g) + } + + return s + }, "") + ) + + const baseGroup = groupBy(dimensionIds, value => { + const [, ...matches] = value.match(groupByRegex) + return matches.join(",") + }) + + let contextAndDimensionsGroup = groupByColumn({}, dimensionIds, groups, tableColumns) + + return [baseGroup, contextAndDimensionsGroup, forRows] + }, [dimensionIds]) + + const columns = useColumns({ + period: tab, + groups, + dimensionIds, + labels, + rowGroups, + contextGroups, + }) + + return ( +
({ key: g, ids: rowGroups[g], contextGroups }))} + // onRowSelected={onItemClick} + // onSearch={noop} + // meta={meta} + // sortBy={sortBy} + // rowSelection={rowSelection} + // onSortingChange={onSortByChange} + // expanded={expanded} + // onExpandedChange={onExpandedChange} + // enableSubRowSelection={enableSubRowSelection} + width="100%" + // bulkActions={bulkActions} + // rowActions={rowActions} + /> + ) +} + +export const TableChart = forwardRef(({ uiName, ...rest }, ref) => ( + + + + + +)) + +export default withChart(TableChart, { tile: true }) diff --git a/src/components/toolbox/chartType.js b/src/components/toolbox/chartType.js index 2b7204db..7a89af5d 100644 --- a/src/components/toolbox/chartType.js +++ b/src/components/toolbox/chartType.js @@ -11,6 +11,7 @@ import easypieChart from "@netdata/netdata-ui/dist/components/icon/assets/chart_ import gaugeChart from "@netdata/netdata-ui/dist/components/icon/assets/chart_gauge.svg" import d3pieChart from "@netdata/netdata-ui/dist/components/icon/assets/chart_pie.svg" import valueChart from "@netdata/netdata-ui/dist/components/icon/assets/value.svg" +import tableChart from "@netdata/netdata-ui/dist/components/icon/assets/chart_bars.svg" import groupBoxesChart from "@netdata/netdata-ui/dist/components/icon/assets/container.svg" import Icon, { Button } from "@/components/icon" import { useChart, useAttributeValue } from "@/components/provider" @@ -60,6 +61,13 @@ const useItems = chart => svg: barChart, "data-track": chart.track("chartType-multiBar"), }, + { + value: "table", + label: "Table", + icon: , + svg: tableChart, + "data-track": chart.track("chartType-tableBar"), + }, { value: "heatmap", label: "Heatmap", diff --git a/src/makeDefaultSDK.js b/src/makeDefaultSDK.js index 7bdecfd8..39a586e8 100644 --- a/src/makeDefaultSDK.js +++ b/src/makeDefaultSDK.js @@ -5,6 +5,7 @@ import number from "./chartLibraries/number" import d3pie from "./chartLibraries/d3pie" import bars from "./chartLibraries/bars" import groupBoxes from "./chartLibraries/groupBoxes" +import table from "./chartLibraries/table" import makeSDK from "./sdk" import unitConversion from "./sdk/plugins/unitConversion" import hover from "./sdk/plugins/hover" @@ -19,7 +20,7 @@ const minutes15 = 15 * 60 export default ({ attributes, ...options } = {}) => makeSDK({ - ui: { dygraph, easypiechart, gauge, groupBoxes, number, d3pie, bars }, + ui: { dygraph, easypiechart, gauge, groupBoxes, number, d3pie, bars, table }, plugins: { // order matters move, diff --git a/src/sdk/makeChart/api/fetchCloudData.js b/src/sdk/makeChart/api/fetchCloudData.js index 8c35a497..10d317d9 100644 --- a/src/sdk/makeChart/api/fetchCloudData.js +++ b/src/sdk/makeChart/api/fetchCloudData.js @@ -37,8 +37,8 @@ const getPayload = chart => { Array.isArray(selectedContexts) && selectedContexts.length ? selectedContexts : context - ? [context] - : wildcardArray, + ? [context] + : wildcardArray, nodes: Array.isArray(selectedNodes) && selectedNodes.length ? selectedNodes : wildcardArray, instances: Array.isArray(selectedInstances) && selectedInstances.length diff --git a/src/sdk/makeChart/filters/makeControllers.js b/src/sdk/makeChart/filters/makeControllers.js index 8900691f..03ef501e 100644 --- a/src/sdk/makeChart/filters/makeControllers.js +++ b/src/sdk/makeChart/filters/makeControllers.js @@ -115,6 +115,7 @@ export default chart => { d3pie: true, bars: true, groupBoxes: true, + table: true, } const updateChartTypeAttribute = selected => { diff --git a/src/sdk/makeChart/makeDimensions.js b/src/sdk/makeChart/makeDimensions.js index ce8862c2..b409ffc8 100644 --- a/src/sdk/makeChart/makeDimensions.js +++ b/src/sdk/makeChart/makeDimensions.js @@ -220,7 +220,7 @@ export default (chart, sdk) => { return memKey } - chart.selectDimensionColor = (id = "selected") => { + chart.selectDimensionColor = (id = "selected", partIndex) => { const key = getMemKey() const colorsAttr = chart.getAttribute("colors") const sparkline = chart.isSparkline() @@ -229,6 +229,8 @@ export default (chart, sdk) => { const isSelected = id === "selected" id = !id || isSelected ? chart.getAttribute("selectedDimensions")[0] : id + if (!isNaN(partIndex)) id = id.split(",")?.[partIndex] || id + const color = isSelected && colorsAttr?.length ? colorsAttr[0] @@ -238,14 +240,18 @@ export default (chart, sdk) => { return typeof color === "string" ? color : color[index] } - chart.getDimensionName = id => { + chart.getDimensionName = (id, partIndex) => { const viewDimensions = chart.getAttribute("viewDimensions") if (!viewDimensions?.names) return "" - if (isHeatmap(chart)) return withoutPrefix(viewDimensions.names[dimensionsById[id]]) + let dimName = viewDimensions.names[dimensionsById[id]] + + if (!isNaN(partIndex)) dimName = dimName.split(",")?.[partIndex] || dimName - return viewDimensions.names[dimensionsById[id]] + if (isHeatmap(chart)) return withoutPrefix(dimName) + + return dimName } chart.getDimensionPriority = id => { @@ -256,6 +262,14 @@ export default (chart, sdk) => { return viewDimensions.priorities[dimensionsById[id]] } + chart.getDimensionGroups = () => { + const viewDimensions = chart.getAttribute("viewDimensions") + + if (!viewDimensions?.grouped_by?.length) return chart.getAttribute("groupBy") + + return viewDimensions.grouped_by + } + chart.getRowDimensionValue = ( id, pointData,