diff --git a/ui/external-plugins/panel/radar/Editor.tsx b/ui/external-plugins/panel/radar/Editor.tsx new file mode 100644 index 000000000..b3e87e6de --- /dev/null +++ b/ui/external-plugins/panel/radar/Editor.tsx @@ -0,0 +1,112 @@ +// Copyright 2023 Datav.io Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { Select, Switch, Textarea } from "@chakra-ui/react" +import PanelAccordion from "src/views/dashboard/edit-panel/Accordion" +import PanelEditItem from "src/views/dashboard/edit-panel/PanelEditItem" +import { Panel, PanelEditorProps } from "types/dashboard" +import React, { memo } from "react"; +import { useStore } from "@nanostores/react" +import { commonMsg, textPanelMsg } from "src/i18n/locales/en" +import { PluginSettings, initSettings, } from "./types" +import { dispatch } from "use-bus"; +import { PanelForceRebuildEvent } from "src/data/bus-events"; +import { defaultsDeep } from "lodash"; +import RadionButtons from "components/RadioButtons"; +import { EditorNumberItem } from "components/editor/EditorItem"; + + +const PanelEditor = memo(({ panel, onChange }: PanelEditorProps) => { + const t = useStore(commonMsg) + panel.plugins[panel.type] = defaultsDeep(panel.plugins[panel.type], initSettings) + const options: PluginSettings = panel.plugins[panel.type] + return ( + <> + + + onChange((panel: Panel) => { + const plugin: PluginSettings = panel.plugins[panel.type] + plugin.animation = e.currentTarget.checked + // force the panel to rebuild to avoid some problems + dispatch(PanelForceRebuildEvent + panel.id) + })} /> + + + + + + + + onChange((panel: Panel) => { + const plugin: PluginSettings = panel.plugins[panel.type] + plugin.graph.legend.mode = v + dispatch(PanelForceRebuildEvent + panel.id) + })} /> + + + + + + { + const plugin: PluginSettings = panel.plugins[panel.type] + plugin.graph.legend.top = e + dispatch(PanelForceRebuildEvent + panel.id) + }} /> + + + { + const plugin: PluginSettings = panel.plugins[panel.type] + plugin.graph.legend.bottom = e + dispatch(PanelForceRebuildEvent + panel.id) + }} /> + + + { + const plugin: PluginSettings = panel.plugins[panel.type] + plugin.graph.legend.left = e + dispatch(PanelForceRebuildEvent + panel.id) + }} /> + + + { + const plugin: PluginSettings = panel.plugins[panel.type] + plugin.graph.legend.right = e + dispatch(PanelForceRebuildEvent + panel.id) + }} /> + + + { + const plugin: PluginSettings = panel.plugins[panel.type] + plugin.graph.legend.itemGap = e + dispatch(PanelForceRebuildEvent + panel.id) + }} /> + + + + ) +}) + +export default PanelEditor \ No newline at end of file diff --git a/ui/external-plugins/panel/radar/OverrideEditor.tsx b/ui/external-plugins/panel/radar/OverrideEditor.tsx new file mode 100644 index 000000000..79146b874 --- /dev/null +++ b/ui/external-plugins/panel/radar/OverrideEditor.tsx @@ -0,0 +1,38 @@ +// Copyright 2023 Datav.io Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { OverrideRule } from "types/dashboard"; +import React from "react"; + +interface Props { + override: OverrideRule + onChange: any +} + + +const OverrideEditor = (props: Props) => { + return <> +} + +export default OverrideEditor + +export enum OverrideRules { + // basic +} + +// The above example will get targets from SeriesData, Table and Graph panels are using this method to get targets +// If return [] or null or undefined, Datav will use the default function to get override targets +export const getOverrideTargets = (panel, data) => { + // for demonstration purpose, we just return a hard coded targets list + return [] +} \ No newline at end of file diff --git a/ui/external-plugins/panel/radar/Panel.tsx b/ui/external-plugins/panel/radar/Panel.tsx new file mode 100644 index 000000000..1d1a3d69d --- /dev/null +++ b/ui/external-plugins/panel/radar/Panel.tsx @@ -0,0 +1,70 @@ +// Copyright 2023 Datav.io Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { Box, Center, Text, useColorMode } from "@chakra-ui/react"; +import ChartComponent from "src/components/charts/Chart"; +import { memo, useMemo, useState } from "react"; +import { PanelProps } from "types/dashboard" +import { FieldType, SeriesData } from "types/seriesData"; +import React from "react"; +import { isEmpty } from "utils/validate"; +import NoData from "src/views/dashboard/components/PanelNoData"; +import { defaultsDeep } from "lodash"; +import { PluginSettings, initSettings } from "./types"; +import { buildOptions } from "./buildOptions"; +import mockData from './mockData.json' +import { isSeriesData } from "utils/seriesData"; + +interface Props extends PanelProps { + data: SeriesData[][] +} + +const PanelComponentWrapper = memo((props: Props) => { + const d: SeriesData[] = props.data.flat() + if (isEmpty(d)) { + return
+ } + + return (<> + + + ) +}) + +export default PanelComponentWrapper + +const PanelComponent = (props: Props) => { + const { panel, height, width } = props + const [chart, setChart] = useState(null) + const { colorMode } = useColorMode() + + + // init panel plugin settings + props.panel.plugins[panel.type] = defaultsDeep(props.panel.plugins[panel.type], initSettings) + // give plugin settings a name for easy access + const options: PluginSettings = props.panel.plugins[panel.type] + + const echartOptions = useMemo(() => { + // transform SeriesData to candlestick data format + const option = buildOptions(panel, props.data.flat(), colorMode) + console.log('====>option:', option) + return option + }, [props.data, panel.overrides, colorMode, options]) + + return (<> + {options && setChart(c)} onChartEvents={null} />} + ) +} + +export const mockDataForTestDataDs = () => { + return mockData +} diff --git a/ui/external-plugins/panel/radar/buildOptions.ts b/ui/external-plugins/panel/radar/buildOptions.ts new file mode 100644 index 000000000..b74bd1758 --- /dev/null +++ b/ui/external-plugins/panel/radar/buildOptions.ts @@ -0,0 +1,49 @@ +import { Panel } from "types/dashboard"; +import { SeriesData } from "types/seriesData"; +import { PluginSettings } from "./types"; + + +export const buildOptions = (panel: Panel, data: SeriesData[], colorMode: "light" | "dark") => { + const options: PluginSettings = panel.plugins[panel.type] + + const legend = data.map(item => item.name) + const seriesData = legend.map(i => ({ name: i, value: [] })) + const indicator = {} + data.forEach(item => { + item.fields.forEach(field => { + const total = field.values.reduce((a, b) => a + b) + if (indicator[field.name] && indicator[field.name] > total) { + indicator[field.name] = field.values.reduce((a, b) => a + b) + } else { + indicator[field.name] = field.values.reduce((a, b) => a + b) + } + const idx = seriesData.findIndex(i => i.name === item.name) + seriesData[idx].value.push(total) + }) + }) + return { + darkMode: colorMode === 'dark', + legend: { + data: legend, + show: options.graph.legend.mode, + top: options.graph.legend.top, + bottom: options.graph.legend.bottom, + left: options.graph.legend.left, + right: options.graph.legend.right, + orient: options.graph.legend.orient, + itemGap: options.graph.legend.itemGap, + }, + animation: options.animation, + radar: { + shape: options.radar.shape, + indicator: Object.keys(indicator).map(key => ({ name: key, value: indicator[key] * 1.25 })), + }, + series: [ + { + type: 'radar', + data: seriesData, + + } + ] + } +} \ No newline at end of file diff --git a/ui/external-plugins/panel/radar/index.ts b/ui/external-plugins/panel/radar/index.ts new file mode 100644 index 000000000..9782e19fc --- /dev/null +++ b/ui/external-plugins/panel/radar/index.ts @@ -0,0 +1,16 @@ +import { PanelPluginComponents } from "types/plugins/plugin"; +import PanelComponent, { mockDataForTestDataDs } from "./Panel"; +import PanelEditor from "./Editor"; +import OverrideEditor, { OverrideRules, getOverrideTargets } from "./OverrideEditor"; + + +const panelComponents: PanelPluginComponents = { + panel: PanelComponent, + editor: PanelEditor, + overrideEditor: OverrideEditor, + overrideRules: OverrideRules, + getOverrideTargets: getOverrideTargets, + mockDataForTestDataDs: mockDataForTestDataDs +} + +export default panelComponents \ No newline at end of file diff --git a/ui/external-plugins/panel/radar/mockData.json b/ui/external-plugins/panel/radar/mockData.json new file mode 100644 index 000000000..e91d4e7d2 --- /dev/null +++ b/ui/external-plugins/panel/radar/mockData.json @@ -0,0 +1,125 @@ +[ + { + "name": "Beijing", + "fields": [ + { + "name": "PM2.5", + "type": "number", + "values": [ + 10, + 20, + 30 + ] + }, + { + "name": "PM10", + "type": "number", + "values": [ + 15, + 25, + 35 + ] + }, + { + "name": "CO", + "type": "number", + "values": [ + 15, + 25, + 35 + ] + }, + { + "name": "NO2", + "type": "number", + "values": [ + 25, + 27, + 30 + ] + } + ] + }, + { + "name": "Shanghai", + "fields": [ + { + "name": "PM2.5", + "type": "number", + "values": [ + 13, + 17, + 20 + ] + }, + { + "name": "PM10", + "type": "number", + "values": [ + 15, + 27, + 40 + ] + }, + { + "name": "CO", + "type": "number", + "values": [ + 35, + 47, + 60 + ] + }, + { + "name": "NO2", + "type": "number", + "values": [ + 35, + 47, + 60 + ] + } + ] + }, + { + "name": "Shenzhen", + "fields": [ + { + "name": "PM2.5", + "type": "number", + "values": [ + 5, + 7, + 13 + ] + }, + { + "name": "PM10", + "type": "number", + "values": [ + 19, + 26, + 50 + ] + }, + { + "name": "CO", + "type": "number", + "values": [ + 15, + 27, + 30 + ] + }, + { + "name": "NO2", + "type": "number", + "values": [ + 42, + 56, + 60 + ] + } + ] + } +] \ No newline at end of file diff --git a/ui/external-plugins/panel/radar/radar.svg b/ui/external-plugins/panel/radar/radar.svg new file mode 100644 index 000000000..345057093 --- /dev/null +++ b/ui/external-plugins/panel/radar/radar.svg @@ -0,0 +1,1593 @@ + + + + diff --git a/ui/external-plugins/panel/radar/types.ts b/ui/external-plugins/panel/radar/types.ts new file mode 100644 index 000000000..9d76a455b --- /dev/null +++ b/ui/external-plugins/panel/radar/types.ts @@ -0,0 +1,34 @@ +export interface PluginSettings { + animation: boolean + graph: { + legend: { + mode: boolean + left?: number + right?: number + top?: number + bottom?: number + orient?: string + itemGap?: number + } + } + radar: { + shape?: string + } +} + + +export const initSettings: PluginSettings = { + animation: false, + radar: { + shape: 'polygon' + }, + graph: { + legend: { + mode: true, + right: 0, + top: 0, + orient: 'horizontal', + itemGap: 20 + } + } +} \ No newline at end of file diff --git a/ui/public/plugins/external/panel/plugins.json b/ui/public/plugins/external/panel/plugins.json index 902e1c742..7211b55ae 100644 --- a/ui/public/plugins/external/panel/plugins.json +++ b/ui/public/plugins/external/panel/plugins.json @@ -1 +1 @@ -[{"type":"candlestick"}] \ No newline at end of file +[{"type":"candlestick"},{"type":"radar"}] \ No newline at end of file diff --git a/ui/public/plugins/external/panel/radar.svg b/ui/public/plugins/external/panel/radar.svg new file mode 100644 index 000000000..17ec18ffe --- /dev/null +++ b/ui/public/plugins/external/panel/radar.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index 854f9ac2a..108e34664 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -16,7 +16,7 @@ export const commonMsg = i18n("common", { "basic": "Basic", "variable": "Variable", "annotation": "Annotation", - "settings": "Settings", + "settings": "Settings", "new": "New", "login": "Sign in", "logout": "Sign out", @@ -97,7 +97,7 @@ export const commonMsg = i18n("common", { "animationTips": "Display chart animation", "display": "Display", "show": "Show", - "calc":"Calculation", + "calc": "Calculation", "calcTips": "calculate results from series data with this reducer function", "value": "Value", "pickColor": "Pick color", @@ -133,7 +133,7 @@ export const commonMsg = i18n("common", { "height": "Height", "sortWeight": "Sort weight", "textinput": "Text input", - "landscapeModeTips":"Please turn your phone to landscape mode for better experience.", + "landscapeModeTips": "Please turn your phone to landscape mode for better experience.", "userStats": "User stats", "builtIn": "Built-in", "external": "External", @@ -155,14 +155,14 @@ export const miscMsg = i18n("misc", { }) export const sidebarMsg = i18n("sidebar", { - "search": "Search", - "selectSidemenu": "Select Sidemenu", - "selectSideMenuTips": "Select a team sidemenu", - "themeChange": "Change Theme", - "accountSetting": "Account", - "adminPanel": "Admin Panel", - "currentLang": "Current Lang", - "newItem": "Add new item", + "search": "Search", + "selectSidemenu": "Select Sidemenu", + "selectSideMenuTips": "Select a team sidemenu", + "themeChange": "Change Theme", + "accountSetting": "Account", + "adminPanel": "Admin Panel", + "currentLang": "Current Lang", + "newItem": "Add new item", }) @@ -243,23 +243,23 @@ export const cfgTeam = i18n("cfgTeam", { "sidemenuTip1": "Customize the top section of your team's side menu, you can add, edit, delete and reorder the menu items.", "sidemenuTip2": "Menu item format", "sidemenuTip3": "Url format", - "level": "Level", + "level": "Level", "sidemenuTip4": "if level 1 is /x, level 2 must be /x/a or /x/b, obviously /y/a is invalid", "sidemenuTip5": "You can find icons in", "modifySidemenu": "Modify Side Menu", "addMenuItem": "Add Menu Item", "removeMenuItem": "Remove Menu Item", "sidemenuErrTitle": "title is required", - "sidemenuErrDashId": "dashboard id is required", + "sidemenuErrDashId": "dashboard id is required", "sidemenuErrLevel1Icon": "Menu item of level 1 must have an icon", "sidemenuErrIcon": params("icon {name} is not exist"), "sidemenuErrUrl": params("{name} is not a valid url"), "sidemenuErrLevel1Url": "level 1 url must be /x, /x/y is invalid", - "sidemenuErrLevel2Url":"level 2 url must use level1 url as prefix", - 'sidemenuErrLevel2Url1': "level 2 url must be /x/y, /x or /x/y/z is invalid", - "sidemenuErrChildTitle": "child title or dashboard id is required", - "sidemenuErrChildUrl": params("{name} is not a valid url"), - "sidemenuReload": "Side menu updated, reloading..." + "sidemenuErrLevel2Url": "level 2 url must use level1 url as prefix", + 'sidemenuErrLevel2Url1': "level 2 url must be /x/y, /x or /x/y/z is invalid", + "sidemenuErrChildTitle": "child title or dashboard id is required", + "sidemenuErrChildUrl": params("{name} is not a valid url"), + "sidemenuReload": "Side menu updated, reloading..." }) @@ -273,7 +273,7 @@ export const newMsg = i18n("new", { "importToast": "Dashboard imported, redirecting...", "jsonInvalid": "Meta json is not valid", "dsToast": "Datasource added, redirecting...", - "testDsFailed": "Test failed", + "testDsFailed": "Test failed", }) @@ -294,8 +294,8 @@ export const dashboardSaveMsg = i18n("dashboardSave", { "autoSaveNotAvail1": "Auto save is not available in history preview mode", "saveMsgRequired": "A save message must be provided when saving in history preview mode", "savedMsg": params("Dashboard {name} saved"), - "saveDueToChanges": "Current dashboard has changes, please save it before viewing history.", - "onPreviewMsg1": "Changed to history preview mode", + "saveDueToChanges": "Current dashboard has changes, please save it before viewing history.", + "onPreviewMsg1": "Changed to history preview mode", "onPreviewMsg2": "Changed to current dashboard", "onPreviewMsg3": "If you want to use preview version, please save it by click save button.", "viewHistory": "View History", @@ -333,7 +333,7 @@ export const dashboardSettingMsg = i18n("dashboardSetting", { "autoSave": "Enable auto save", "autoSaveTips": "Dashboard will be auto saved at intervals, you can find old versions in save history list", "autoSaveInterval": "Auto save interval(seconds)", - "hiddenPanel": "Hidden panels", + "hiddenPanel": "Hidden panels", "hiddenPanelTips": "You can hide a panel by clicking its header and select hide panel", "sortWeight": "Sort priority", "sortWeightTips": "Higher value means higher sort priority, this is used in Search dashboards", @@ -344,7 +344,7 @@ export const dashboardSettingMsg = i18n("dashboardSetting", { "backgroundColorModeTips": "Change to this color mode when using background image", "enableBg": "Enable background", "enableBgTips": "Whether using the background image set above", - "dashBorder": "Dashboard border", + "dashBorder": "Dashboard border", "dashBorderTips": "Select a cool border for your dashboard", "dashSaved": "Dashboard saved", @@ -414,7 +414,7 @@ export const panelMsg = i18n("panel", { "transformTips": `Define a function to transform the panel data query from datasource into the format which the panel chart requires`, "enableTransform": "Whether enable transform", - "conditionRender": "Conditional render", + "conditionRender": "Conditional render", "conditionRenderTips": "If the condition you set is satisfied, the panel will be rendered, otherwise it will be hidden", "condition": "Condition", "conditionTips": "Check a variable is set to a given value" @@ -491,7 +491,7 @@ export const nodeGraphPanelMsg = i18n("nodeGraphPanel", { "highlightNodesInputTips": "support multiple regex, split with comma", "invalidHighlight": "Invalid highlight function", "highlightColor": "Highlight color", - "pickLightColor": "Pick light color", + "pickLightColor": "Pick light color", "pickDarkColor": "Pick dark color", "tooltipTrigger": "Tooltip trigger", "layout": "Layout", @@ -509,7 +509,7 @@ export const nodeGraphPanelMsg = i18n("nodeGraphPanel", { "quadratic": "Quadratic", "polyline": "Polyline", "edgeColor": "Edge color", - "noAttrsToSet" : "No attrs to set" + "noAttrsToSet": "No attrs to set" }) export const echartsPanelMsg = i18n("echartsPanel", { @@ -524,7 +524,7 @@ export const echartsPanelMsg = i18n("echartsPanel", { "liveEdit": params("Live Edit( fetch data from {name} datasource)"), "regEvents": "Register events function", "regEventsTips": "custom your chart events, e.g mouseclick, mouseover etc", - "editRegFunc":"Edit registerEvents function", + "editRegFunc": "Edit registerEvents function", }) export const textPanelMsg = i18n("textPanel", { @@ -535,19 +535,19 @@ export const textPanelMsg = i18n("textPanel", { "left": "Left", "center": "Center", "right": "Right", - "top": "Top", + "top": "Top", "bottom": "Bottom", }) export const piePanelMsg = i18n("piePanel", { - "showLabel": "Show label", - "showLabelTips": "When view in mobile screen, show label will automatically become show legend for better user experience", - "shape": "Shape", - "borderRadius": "Border radius", - "pieRadius": "Pie radius", - "innerRadius": "Inner radius", - "orient": "Orient", - "placement": "Placement" + "showLabel": "Show label", + "showLabelTips": "When view in mobile screen, show label will automatically become show legend for better user experience", + "shape": "Shape", + "borderRadius": "Border radius", + "pieRadius": "Pie radius", + "innerRadius": "Inner radius", + "orient": "Orient", + "placement": "Placement" }) export const gaugePanelMsg = i18n("gaugePanel", { @@ -572,24 +572,24 @@ export const statsPanelMsg = i18n("statsPanel", { }) export const tracePanelMsg = i18n("tracePanel", { - "maxDuration": "Max duration", - "minDuration": "Min duration", - "limitResults": "Limit results", - "findTraces": "Find traces", - "useLatestTime": "Use latest time", - "tracesTotal": "Traces Total", - "tracesSelected": "Traces Selected", - "clearSelection": "Clear selection", - "recent": "Most Recent", - "mostErrors": "Most Errors", - "longest": "Longest Duration", - "shortest": "Shortest Duration", - "mostSpans": "Most Spans", - "leastSpans": "Least Spans", - "traceIdsTips": "Searching by trace ids has the highest priority, so if you want to search with options, leave this empty", - "traceIdsInputTips": "search with trace ids, separated with comma", - "selectForCompre": "selected for comparison", - "startTime": "Start time", + "maxDuration": "Max duration", + "minDuration": "Min duration", + "limitResults": "Limit results", + "findTraces": "Find traces", + "useLatestTime": "Use latest time", + "tracesTotal": "Traces Total", + "tracesSelected": "Traces Selected", + "clearSelection": "Clear selection", + "recent": "Most Recent", + "mostErrors": "Most Errors", + "longest": "Longest Duration", + "shortest": "Shortest Duration", + "mostSpans": "Most Spans", + "leastSpans": "Least Spans", + "traceIdsTips": "Searching by trace ids has the highest priority, so if you want to search with options, leave this empty", + "traceIdsInputTips": "search with trace ids, separated with comma", + "selectForCompre": "selected for comparison", + "startTime": "Start time", }) @@ -606,32 +606,32 @@ export const componentsMsg = i18n("components", { }) export const tablePanelMsg = i18n("tablePanel", { - "tableSetting": "Table Setting", - "showHeader": "Show header", - "showHeaderTips": "whether display table's header", - "showBorder": "Show border", - "stickyHeader": "Sticky header", - "stickyHeaderTips": "fix header to top, useful for viewing many rows in one page", - "cellSize": "Cell size", - "tableWidth": "Table width", - "column": "Column", - "columnAlignment": "Column alignment", - "columnSort": "Column sort", - "columnSortTips": "click the column title to sort it by asc or desc", - "columnFilter": "Column filter", - "columnFilterTips": "filter the column values in table", - "onRowClick": "On row click", - "onRowClickTips": "when click on a row, this event will be executed", - "rowActions": "Click actions", - "rowActionsTips": "add some actions to panel, e.g edit, delete", - "addAction": "Add action", - "actionColumnName": "Action column name", - "actionColumnWidth": "Action column width", - "actionButtonSize": "Action button size", - "seriesName": "change column display name", - "seriesFilter1": "Number min/max", - "seriesFilter2": "String match", - "colorTitle": "Title color" + "tableSetting": "Table Setting", + "showHeader": "Show header", + "showHeaderTips": "whether display table's header", + "showBorder": "Show border", + "stickyHeader": "Sticky header", + "stickyHeaderTips": "fix header to top, useful for viewing many rows in one page", + "cellSize": "Cell size", + "tableWidth": "Table width", + "column": "Column", + "columnAlignment": "Column alignment", + "columnSort": "Column sort", + "columnSortTips": "click the column title to sort it by asc or desc", + "columnFilter": "Column filter", + "columnFilterTips": "filter the column values in table", + "onRowClick": "On row click", + "onRowClickTips": "when click on a row, this event will be executed", + "rowActions": "Click actions", + "rowActionsTips": "add some actions to panel, e.g edit, delete", + "addAction": "Add action", + "actionColumnName": "Action column name", + "actionColumnWidth": "Action column width", + "actionButtonSize": "Action button size", + "seriesName": "change column display name", + "seriesFilter1": "Number min/max", + "seriesFilter2": "String match", + "colorTitle": "Title color" }) export const barGaugePanelMsg = i18n("barGaugePanel", { @@ -649,15 +649,15 @@ export const barGaugePanelMsg = i18n("barGaugePanel", { "showUnfilledTips": "When enabled renders the unfilled region as gray", "titleSize": "Title font size", "valueSize": "Value font size", - "layoutDir" : "Layout direction", + "layoutDir": "Layout direction", }) export const ValueMappingMsg = i18n("valueMapping", { - + }) export const alertMsg = i18n("alert", { - "alertFilter": "Alert filter", - "alertState": "Alert state", - "datasourceTips": "Query alerts from these datasources" + "alertFilter": "Alert filter", + "alertState": "Alert state", + "datasourceTips": "Query alerts from these datasources" }) diff --git a/ui/src/views/dashboard/plugins/external/panel/radar/Editor.tsx b/ui/src/views/dashboard/plugins/external/panel/radar/Editor.tsx new file mode 100644 index 000000000..b3e87e6de --- /dev/null +++ b/ui/src/views/dashboard/plugins/external/panel/radar/Editor.tsx @@ -0,0 +1,112 @@ +// Copyright 2023 Datav.io Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { Select, Switch, Textarea } from "@chakra-ui/react" +import PanelAccordion from "src/views/dashboard/edit-panel/Accordion" +import PanelEditItem from "src/views/dashboard/edit-panel/PanelEditItem" +import { Panel, PanelEditorProps } from "types/dashboard" +import React, { memo } from "react"; +import { useStore } from "@nanostores/react" +import { commonMsg, textPanelMsg } from "src/i18n/locales/en" +import { PluginSettings, initSettings, } from "./types" +import { dispatch } from "use-bus"; +import { PanelForceRebuildEvent } from "src/data/bus-events"; +import { defaultsDeep } from "lodash"; +import RadionButtons from "components/RadioButtons"; +import { EditorNumberItem } from "components/editor/EditorItem"; + + +const PanelEditor = memo(({ panel, onChange }: PanelEditorProps) => { + const t = useStore(commonMsg) + panel.plugins[panel.type] = defaultsDeep(panel.plugins[panel.type], initSettings) + const options: PluginSettings = panel.plugins[panel.type] + return ( + <> + + + onChange((panel: Panel) => { + const plugin: PluginSettings = panel.plugins[panel.type] + plugin.animation = e.currentTarget.checked + // force the panel to rebuild to avoid some problems + dispatch(PanelForceRebuildEvent + panel.id) + })} /> + + + + + + + + onChange((panel: Panel) => { + const plugin: PluginSettings = panel.plugins[panel.type] + plugin.graph.legend.mode = v + dispatch(PanelForceRebuildEvent + panel.id) + })} /> + + + + + + { + const plugin: PluginSettings = panel.plugins[panel.type] + plugin.graph.legend.top = e + dispatch(PanelForceRebuildEvent + panel.id) + }} /> + + + { + const plugin: PluginSettings = panel.plugins[panel.type] + plugin.graph.legend.bottom = e + dispatch(PanelForceRebuildEvent + panel.id) + }} /> + + + { + const plugin: PluginSettings = panel.plugins[panel.type] + plugin.graph.legend.left = e + dispatch(PanelForceRebuildEvent + panel.id) + }} /> + + + { + const plugin: PluginSettings = panel.plugins[panel.type] + plugin.graph.legend.right = e + dispatch(PanelForceRebuildEvent + panel.id) + }} /> + + + { + const plugin: PluginSettings = panel.plugins[panel.type] + plugin.graph.legend.itemGap = e + dispatch(PanelForceRebuildEvent + panel.id) + }} /> + + + + ) +}) + +export default PanelEditor \ No newline at end of file diff --git a/ui/src/views/dashboard/plugins/external/panel/radar/OverrideEditor.tsx b/ui/src/views/dashboard/plugins/external/panel/radar/OverrideEditor.tsx new file mode 100644 index 000000000..79146b874 --- /dev/null +++ b/ui/src/views/dashboard/plugins/external/panel/radar/OverrideEditor.tsx @@ -0,0 +1,38 @@ +// Copyright 2023 Datav.io Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { OverrideRule } from "types/dashboard"; +import React from "react"; + +interface Props { + override: OverrideRule + onChange: any +} + + +const OverrideEditor = (props: Props) => { + return <> +} + +export default OverrideEditor + +export enum OverrideRules { + // basic +} + +// The above example will get targets from SeriesData, Table and Graph panels are using this method to get targets +// If return [] or null or undefined, Datav will use the default function to get override targets +export const getOverrideTargets = (panel, data) => { + // for demonstration purpose, we just return a hard coded targets list + return [] +} \ No newline at end of file diff --git a/ui/src/views/dashboard/plugins/external/panel/radar/Panel.tsx b/ui/src/views/dashboard/plugins/external/panel/radar/Panel.tsx new file mode 100644 index 000000000..1d1a3d69d --- /dev/null +++ b/ui/src/views/dashboard/plugins/external/panel/radar/Panel.tsx @@ -0,0 +1,70 @@ +// Copyright 2023 Datav.io Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { Box, Center, Text, useColorMode } from "@chakra-ui/react"; +import ChartComponent from "src/components/charts/Chart"; +import { memo, useMemo, useState } from "react"; +import { PanelProps } from "types/dashboard" +import { FieldType, SeriesData } from "types/seriesData"; +import React from "react"; +import { isEmpty } from "utils/validate"; +import NoData from "src/views/dashboard/components/PanelNoData"; +import { defaultsDeep } from "lodash"; +import { PluginSettings, initSettings } from "./types"; +import { buildOptions } from "./buildOptions"; +import mockData from './mockData.json' +import { isSeriesData } from "utils/seriesData"; + +interface Props extends PanelProps { + data: SeriesData[][] +} + +const PanelComponentWrapper = memo((props: Props) => { + const d: SeriesData[] = props.data.flat() + if (isEmpty(d)) { + return
+ } + + return (<> + + + ) +}) + +export default PanelComponentWrapper + +const PanelComponent = (props: Props) => { + const { panel, height, width } = props + const [chart, setChart] = useState(null) + const { colorMode } = useColorMode() + + + // init panel plugin settings + props.panel.plugins[panel.type] = defaultsDeep(props.panel.plugins[panel.type], initSettings) + // give plugin settings a name for easy access + const options: PluginSettings = props.panel.plugins[panel.type] + + const echartOptions = useMemo(() => { + // transform SeriesData to candlestick data format + const option = buildOptions(panel, props.data.flat(), colorMode) + console.log('====>option:', option) + return option + }, [props.data, panel.overrides, colorMode, options]) + + return (<> + {options && setChart(c)} onChartEvents={null} />} + ) +} + +export const mockDataForTestDataDs = () => { + return mockData +} diff --git a/ui/src/views/dashboard/plugins/external/panel/radar/buildOptions.ts b/ui/src/views/dashboard/plugins/external/panel/radar/buildOptions.ts new file mode 100644 index 000000000..b74bd1758 --- /dev/null +++ b/ui/src/views/dashboard/plugins/external/panel/radar/buildOptions.ts @@ -0,0 +1,49 @@ +import { Panel } from "types/dashboard"; +import { SeriesData } from "types/seriesData"; +import { PluginSettings } from "./types"; + + +export const buildOptions = (panel: Panel, data: SeriesData[], colorMode: "light" | "dark") => { + const options: PluginSettings = panel.plugins[panel.type] + + const legend = data.map(item => item.name) + const seriesData = legend.map(i => ({ name: i, value: [] })) + const indicator = {} + data.forEach(item => { + item.fields.forEach(field => { + const total = field.values.reduce((a, b) => a + b) + if (indicator[field.name] && indicator[field.name] > total) { + indicator[field.name] = field.values.reduce((a, b) => a + b) + } else { + indicator[field.name] = field.values.reduce((a, b) => a + b) + } + const idx = seriesData.findIndex(i => i.name === item.name) + seriesData[idx].value.push(total) + }) + }) + return { + darkMode: colorMode === 'dark', + legend: { + data: legend, + show: options.graph.legend.mode, + top: options.graph.legend.top, + bottom: options.graph.legend.bottom, + left: options.graph.legend.left, + right: options.graph.legend.right, + orient: options.graph.legend.orient, + itemGap: options.graph.legend.itemGap, + }, + animation: options.animation, + radar: { + shape: options.radar.shape, + indicator: Object.keys(indicator).map(key => ({ name: key, value: indicator[key] * 1.25 })), + }, + series: [ + { + type: 'radar', + data: seriesData, + + } + ] + } +} \ No newline at end of file diff --git a/ui/src/views/dashboard/plugins/external/panel/radar/index.ts b/ui/src/views/dashboard/plugins/external/panel/radar/index.ts new file mode 100644 index 000000000..9782e19fc --- /dev/null +++ b/ui/src/views/dashboard/plugins/external/panel/radar/index.ts @@ -0,0 +1,16 @@ +import { PanelPluginComponents } from "types/plugins/plugin"; +import PanelComponent, { mockDataForTestDataDs } from "./Panel"; +import PanelEditor from "./Editor"; +import OverrideEditor, { OverrideRules, getOverrideTargets } from "./OverrideEditor"; + + +const panelComponents: PanelPluginComponents = { + panel: PanelComponent, + editor: PanelEditor, + overrideEditor: OverrideEditor, + overrideRules: OverrideRules, + getOverrideTargets: getOverrideTargets, + mockDataForTestDataDs: mockDataForTestDataDs +} + +export default panelComponents \ No newline at end of file diff --git a/ui/src/views/dashboard/plugins/external/panel/radar/mockData.json b/ui/src/views/dashboard/plugins/external/panel/radar/mockData.json new file mode 100644 index 000000000..e91d4e7d2 --- /dev/null +++ b/ui/src/views/dashboard/plugins/external/panel/radar/mockData.json @@ -0,0 +1,125 @@ +[ + { + "name": "Beijing", + "fields": [ + { + "name": "PM2.5", + "type": "number", + "values": [ + 10, + 20, + 30 + ] + }, + { + "name": "PM10", + "type": "number", + "values": [ + 15, + 25, + 35 + ] + }, + { + "name": "CO", + "type": "number", + "values": [ + 15, + 25, + 35 + ] + }, + { + "name": "NO2", + "type": "number", + "values": [ + 25, + 27, + 30 + ] + } + ] + }, + { + "name": "Shanghai", + "fields": [ + { + "name": "PM2.5", + "type": "number", + "values": [ + 13, + 17, + 20 + ] + }, + { + "name": "PM10", + "type": "number", + "values": [ + 15, + 27, + 40 + ] + }, + { + "name": "CO", + "type": "number", + "values": [ + 35, + 47, + 60 + ] + }, + { + "name": "NO2", + "type": "number", + "values": [ + 35, + 47, + 60 + ] + } + ] + }, + { + "name": "Shenzhen", + "fields": [ + { + "name": "PM2.5", + "type": "number", + "values": [ + 5, + 7, + 13 + ] + }, + { + "name": "PM10", + "type": "number", + "values": [ + 19, + 26, + 50 + ] + }, + { + "name": "CO", + "type": "number", + "values": [ + 15, + 27, + 30 + ] + }, + { + "name": "NO2", + "type": "number", + "values": [ + 42, + 56, + 60 + ] + } + ] + } +] \ No newline at end of file diff --git a/ui/src/views/dashboard/plugins/external/panel/radar/radar.svg b/ui/src/views/dashboard/plugins/external/panel/radar/radar.svg new file mode 100644 index 000000000..345057093 --- /dev/null +++ b/ui/src/views/dashboard/plugins/external/panel/radar/radar.svg @@ -0,0 +1,1593 @@ + + + + diff --git a/ui/src/views/dashboard/plugins/external/panel/radar/types.ts b/ui/src/views/dashboard/plugins/external/panel/radar/types.ts new file mode 100644 index 000000000..9d76a455b --- /dev/null +++ b/ui/src/views/dashboard/plugins/external/panel/radar/types.ts @@ -0,0 +1,34 @@ +export interface PluginSettings { + animation: boolean + graph: { + legend: { + mode: boolean + left?: number + right?: number + top?: number + bottom?: number + orient?: string + itemGap?: number + } + } + radar: { + shape?: string + } +} + + +export const initSettings: PluginSettings = { + animation: false, + radar: { + shape: 'polygon' + }, + graph: { + legend: { + mode: true, + right: 0, + top: 0, + orient: 'horizontal', + itemGap: 20 + } + } +} \ No newline at end of file diff --git a/ui/src/views/dashboard/plugins/external/plugins.ts b/ui/src/views/dashboard/plugins/external/plugins.ts index dfb52fef0..88dbb4e63 100644 --- a/ui/src/views/dashboard/plugins/external/plugins.ts +++ b/ui/src/views/dashboard/plugins/external/plugins.ts @@ -3,9 +3,11 @@ import { DatasourcePluginComponents, PanelPluginComponents } from "types/plugins/plugin" import CandlestickComponents from "./panel/candlestick" +import RadarComponents from "./panel/radar" import VictoriaMetricsDatasrouceComponents from "./datasource/victoriaMetrics" export const externalPanelPlugins: Record = { "candlestick": CandlestickComponents, + "radar": RadarComponents, } export const externalDatasourcePlugins: Record = { "victoriaMetrics": VictoriaMetricsDatasrouceComponents,