From 6b2043c8326a13f3abebc0c195020ee441382eff Mon Sep 17 00:00:00 2001 From: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> Date: Fri, 21 May 2021 10:24:16 -0700 Subject: [PATCH] Dashboard widgets async (#762) * Make Widgets load asynchronously Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> * Added loading spinners to all widgets Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> --- models/widget.go | 3 + models/widget_details.go | 222 +++++++ .../Dashboard/Prometheus/PrDashboard.tsx | 42 +- .../Prometheus/Widgets/BarChartWidget.tsx | 145 +++-- .../Prometheus/Widgets/LinearGraphWidget.tsx | 211 ++++--- .../Prometheus/Widgets/PieChartWidget.tsx | 277 +++++---- .../Prometheus/Widgets/SingleRepWidget.tsx | 127 +++- .../Prometheus/Widgets/SingleValueWidget.tsx | 74 ++- .../Console/Dashboard/Prometheus/types.ts | 1 + .../Console/Dashboard/Prometheus/utils.ts | 542 ++++++++++-------- restapi/admin_info.go | 286 ++++----- restapi/embedded_spec.go | 173 +++++- .../admin_api/admin_info_parameters.go | 99 ---- .../admin_api/admin_info_urlbuilder.go | 36 -- .../admin_api/dashboard_widget_details.go | 90 +++ .../dashboard_widget_details_parameters.go | 190 ++++++ .../dashboard_widget_details_responses.go | 133 +++++ .../dashboard_widget_details_urlbuilder.go | 150 +++++ restapi/operations/console_api.go | 12 + swagger.yml | 50 +- 20 files changed, 2062 insertions(+), 801 deletions(-) create mode 100644 models/widget_details.go create mode 100644 restapi/operations/admin_api/dashboard_widget_details.go create mode 100644 restapi/operations/admin_api/dashboard_widget_details_parameters.go create mode 100644 restapi/operations/admin_api/dashboard_widget_details_responses.go create mode 100644 restapi/operations/admin_api/dashboard_widget_details_urlbuilder.go diff --git a/models/widget.go b/models/widget.go index c75f8fc3d..86290656f 100644 --- a/models/widget.go +++ b/models/widget.go @@ -35,6 +35,9 @@ import ( // swagger:model widget type Widget struct { + // id + ID int32 `json:"id,omitempty"` + // options Options *WidgetOptions `json:"options,omitempty"` diff --git a/models/widget_details.go b/models/widget_details.go new file mode 100644 index 000000000..3f09dbe80 --- /dev/null +++ b/models/widget_details.go @@ -0,0 +1,222 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// WidgetDetails widget details +// +// swagger:model widgetDetails +type WidgetDetails struct { + + // id + ID int32 `json:"id,omitempty"` + + // options + Options *WidgetDetailsOptions `json:"options,omitempty"` + + // targets + Targets []*ResultTarget `json:"targets"` + + // title + Title string `json:"title,omitempty"` + + // type + Type string `json:"type,omitempty"` +} + +// Validate validates this widget details +func (m *WidgetDetails) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateOptions(formats); err != nil { + res = append(res, err) + } + + if err := m.validateTargets(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *WidgetDetails) validateOptions(formats strfmt.Registry) error { + + if swag.IsZero(m.Options) { // not required + return nil + } + + if m.Options != nil { + if err := m.Options.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("options") + } + return err + } + } + + return nil +} + +func (m *WidgetDetails) validateTargets(formats strfmt.Registry) error { + + if swag.IsZero(m.Targets) { // not required + return nil + } + + for i := 0; i < len(m.Targets); i++ { + if swag.IsZero(m.Targets[i]) { // not required + continue + } + + if m.Targets[i] != nil { + if err := m.Targets[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("targets" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *WidgetDetails) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *WidgetDetails) UnmarshalBinary(b []byte) error { + var res WidgetDetails + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// WidgetDetailsOptions widget details options +// +// swagger:model WidgetDetailsOptions +type WidgetDetailsOptions struct { + + // reduce options + ReduceOptions *WidgetDetailsOptionsReduceOptions `json:"reduceOptions,omitempty"` +} + +// Validate validates this widget details options +func (m *WidgetDetailsOptions) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateReduceOptions(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *WidgetDetailsOptions) validateReduceOptions(formats strfmt.Registry) error { + + if swag.IsZero(m.ReduceOptions) { // not required + return nil + } + + if m.ReduceOptions != nil { + if err := m.ReduceOptions.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("options" + "." + "reduceOptions") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *WidgetDetailsOptions) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *WidgetDetailsOptions) UnmarshalBinary(b []byte) error { + var res WidgetDetailsOptions + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// WidgetDetailsOptionsReduceOptions widget details options reduce options +// +// swagger:model WidgetDetailsOptionsReduceOptions +type WidgetDetailsOptionsReduceOptions struct { + + // calcs + Calcs []string `json:"calcs"` +} + +// Validate validates this widget details options reduce options +func (m *WidgetDetailsOptionsReduceOptions) Validate(formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *WidgetDetailsOptionsReduceOptions) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *WidgetDetailsOptionsReduceOptions) UnmarshalBinary(b []byte) error { + var res WidgetDetailsOptionsReduceOptions + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/portal-ui/src/screens/Console/Dashboard/Prometheus/PrDashboard.tsx b/portal-ui/src/screens/Console/Dashboard/Prometheus/PrDashboard.tsx index bb32cb1d3..d1138b71c 100644 --- a/portal-ui/src/screens/Console/Dashboard/Prometheus/PrDashboard.tsx +++ b/portal-ui/src/screens/Console/Dashboard/Prometheus/PrDashboard.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { useEffect, useState, useCallback } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { connect } from "react-redux"; import ReactGridLayout from "react-grid-layout"; import Grid from "@material-ui/core/Grid"; @@ -26,12 +26,6 @@ import { } from "../../Common/FormComponents/common/styleLibrary"; import { AutoSizer } from "react-virtualized"; -import { - IBarChartConfiguration, - IDataSRep, - ILinearGraphConfiguration, - IPieChartConfiguration, -} from "./Widgets/types"; import { IDashboardPanel, widgetType } from "./types"; import { getDashboardDistribution, @@ -104,29 +98,27 @@ const PrDashboard = ({ classes, displayErrorMessage }: IPrDashboard) => { return ( ); case widgetType.pieChart: return ( ); case widgetType.linearGraph: return ( { return ( ); case widgetType.singleRep: @@ -152,8 +143,9 @@ const PrDashboard = ({ classes, displayErrorMessage }: IPrDashboard) => { return ( @@ -169,7 +161,7 @@ const PrDashboard = ({ classes, displayErrorMessage }: IPrDashboard) => { ); }); }, - [panelInformation, dashboardDistr] + [panelInformation, dashboardDistr, timeEnd, timeStart] ); const fetchUsage = useCallback(() => { diff --git a/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/BarChartWidget.tsx b/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/BarChartWidget.tsx index a0d3b7663..1201c1b43 100644 --- a/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/BarChartWidget.tsx +++ b/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/BarChartWidget.tsx @@ -14,30 +14,45 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React from "react"; +import React, { useEffect, useState } from "react"; import { Bar, BarChart, ResponsiveContainer, + Tooltip, XAxis, YAxis, - Tooltip, } from "recharts"; import { IBarChartConfiguration } from "./types"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { widgetCommon } from "../../../Common/FormComponents/common/styleLibrary"; import BarChartTooltip from "./tooltips/BarChartTooltip"; +import { setErrorSnackMessage } from "../../../../../actions"; +import { connect } from "react-redux"; +import { IDashboardPanel } from "../types"; +import { MaterialUiPickersDate } from "@material-ui/pickers/typings/date"; +import api from "../../../../../common/api"; +import { widgetDetailsToPanel } from "../utils"; +import { CircularProgress } from "@material-ui/core"; interface IBarChartWidget { classes: any; title: string; - barChartConfiguration: IBarChartConfiguration[]; - data: object[]; + panelItem: IDashboardPanel; + timeStart: MaterialUiPickersDate; + timeEnd: MaterialUiPickersDate; + displayErrorMessage: any; } const styles = (theme: Theme) => createStyles({ ...widgetCommon, + loadingAlign: { + width: "100%", + paddingTop: "15px", + textAlign: "center", + margin: "auto", + }, }); const CustomizedAxisTick = ({ x, y, payload }: any) => { @@ -58,46 +73,102 @@ const CustomizedAxisTick = ({ x, y, payload }: any) => { const BarChartWidget = ({ classes, title, - barChartConfiguration, - data, + panelItem, + timeStart, + timeEnd, + displayErrorMessage, }: IBarChartWidget) => { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + const [result, setResult] = useState(null); + useEffect(() => { + if (loading) { + let stepCalc = 0; + if (timeStart !== null && timeEnd !== null) { + const secondsInPeriod = timeEnd.unix() - timeStart.unix(); + const periods = Math.floor(secondsInPeriod / 60); + + stepCalc = periods < 1 ? 15 : periods; + } + + api + .invoke( + "GET", + `/api/v1/admin/info/widgets/${panelItem.id}/?step=${stepCalc}&${ + timeStart !== null ? `&start=${timeStart.unix()}` : "" + }${timeStart !== null && timeEnd !== null ? "&" : ""}${ + timeEnd !== null ? `end=${timeEnd.unix()}` : "" + }` + ) + .then((res: any) => { + const widgetsWithValue = widgetDetailsToPanel(res, panelItem); + setData(widgetsWithValue.data); + setResult(widgetsWithValue); + setLoading(false); + }) + .catch((err) => { + displayErrorMessage(err); + setLoading(false); + }); + } + }, [loading, panelItem, timeEnd, timeStart, displayErrorMessage]); + + const barChartConfiguration = result + ? (result.widgetConfiguration as IBarChartConfiguration[]) + : []; + return ( {title} - - - - - } - tickLine={false} - axisLine={false} - width={150} - /> - {barChartConfiguration.map((bar) => ( - + + + )} + {!loading && ( + + + + + } + tickLine={false} + axisLine={false} + width={150} /> - ))} - ( + - } - /> - - - + ))} + + } + /> + + + + )} ); }; -export default withStyles(styles)(BarChartWidget); +const connector = connect(null, { + displayErrorMessage: setErrorSnackMessage, +}); + +export default withStyles(styles)(connector(BarChartWidget)); diff --git a/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/LinearGraphWidget.tsx b/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/LinearGraphWidget.tsx index 5c3687119..233b7fbfc 100644 --- a/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/LinearGraphWidget.tsx +++ b/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/LinearGraphWidget.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React from "react"; +import React, { useEffect, useState } from "react"; import { Area, AreaChart, @@ -28,12 +28,21 @@ import { ILinearGraphConfiguration } from "./types"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { widgetCommon } from "../../../Common/FormComponents/common/styleLibrary"; import LineChartTooltip from "./tooltips/LineChartTooltip"; +import { IDashboardPanel } from "../types"; +import { MaterialUiPickersDate } from "@material-ui/pickers/typings/date"; +import { connect } from "react-redux"; +import { setErrorSnackMessage } from "../../../../../actions"; +import api from "../../../../../common/api"; +import { widgetDetailsToPanel } from "../utils"; +import { CircularProgress } from "@material-ui/core"; interface ILinearGraphWidget { classes: any; title: string; - linearConfiguration: ILinearGraphConfiguration[]; - data: object[]; + panelItem: IDashboardPanel; + timeStart: MaterialUiPickersDate; + timeEnd: MaterialUiPickersDate; + displayErrorMessage: any; hideYAxis?: boolean; yAxisFormatter?: (item: string) => string; xAxisFormatter?: (item: string) => string; @@ -64,18 +73,58 @@ const styles = (theme: Theme) => position: "relative", textAlign: "center", }, + loadingAlign: { + margin: "auto", + }, }); const LinearGraphWidget = ({ classes, title, - linearConfiguration, - data, + displayErrorMessage, + timeStart, + timeEnd, + panelItem, hideYAxis = false, yAxisFormatter = (item: string) => item, xAxisFormatter = (item: string) => item, panelWidth = 0, }: ILinearGraphWidget) => { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + const [result, setResult] = useState(null); + useEffect(() => { + if (loading) { + let stepCalc = 0; + if (timeStart !== null && timeEnd !== null) { + const secondsInPeriod = timeEnd.unix() - timeStart.unix(); + const periods = Math.floor(secondsInPeriod / 60); + + stepCalc = periods < 1 ? 15 : periods; + } + + api + .invoke( + "GET", + `/api/v1/admin/info/widgets/${panelItem.id}/?step=${stepCalc}&${ + timeStart !== null ? `&start=${timeStart.unix()}` : "" + }${timeStart !== null && timeEnd !== null ? "&" : ""}${ + timeEnd !== null ? `end=${timeEnd.unix()}` : "" + }` + ) + .then((res: any) => { + const widgetsWithValue = widgetDetailsToPanel(res, panelItem); + setData(widgetsWithValue.data); + setResult(widgetsWithValue); + setLoading(false); + }) + .catch((err) => { + displayErrorMessage(err); + setLoading(false); + }); + } + }, [loading, panelItem, timeEnd, timeStart, displayErrorMessage]); + let intervalCount = 5; if (panelWidth !== 0) { @@ -91,84 +140,100 @@ const LinearGraphWidget = ({ intervalCount = 30; } } + + const linearConfiguration = result + ? (result?.widgetConfiguration as ILinearGraphConfiguration[]) + : []; + return ( {title} - - - - - xAxisFormatter(value)} - interval={intervalCount} - tick={{ fontSize: "70%" }} - tickCount={10} - /> - dataMax * 4]} - hide={hideYAxis} - tickFormatter={(value: any) => yAxisFormatter(value)} - tick={{ fontSize: "70%" }} - /> + {loading && } + {!loading && ( + + + + + + xAxisFormatter(value)} + interval={intervalCount} + tick={{ fontSize: "70%" }} + tickCount={10} + /> + dataMax * 4]} + hide={hideYAxis} + tickFormatter={(value: any) => yAxisFormatter(value)} + tick={{ fontSize: "70%" }} + /> + {linearConfiguration.map((section, index) => { + return ( + + ); + })} + + } + wrapperStyle={{ + zIndex: 5000, + }} + /> + + + + {linearConfiguration.map((section, index) => { return ( - + + + + {section.keyLabel} + + ); })} - - } - wrapperStyle={{ - zIndex: 5000, - }} - /> - - - - - {linearConfiguration.map((section, index) => { - return ( - - - {section.keyLabel} - - ); - })} - + + + )} ); }; -export default withStyles(styles)(LinearGraphWidget); +const connector = connect(null, { + displayErrorMessage: setErrorSnackMessage, +}); + +export default withStyles(styles)(connector(LinearGraphWidget)); diff --git a/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/PieChartWidget.tsx b/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/PieChartWidget.tsx index e0fd46228..68cd7ce22 100644 --- a/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/PieChartWidget.tsx +++ b/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/PieChartWidget.tsx @@ -14,143 +14,210 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React from "react"; +import React, { useEffect, useState } from "react"; import get from "lodash/get"; import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts"; import { IPieChartConfiguration } from "./types"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { widgetCommon } from "../../../Common/FormComponents/common/styleLibrary"; +import { connect } from "react-redux"; +import { setErrorSnackMessage } from "../../../../../actions"; +import { IDashboardPanel } from "../types"; +import { MaterialUiPickersDate } from "@material-ui/pickers/typings/date"; +import api from "../../../../../common/api"; +import { widgetDetailsToPanel } from "../utils"; +import { CircularProgress } from "@material-ui/core"; interface IPieChartWidget { classes: any; title: string; - pieChartConfiguration: IPieChartConfiguration; - dataInner: object[]; - dataOuter?: object[]; - middleLabel?: string; + panelItem: IDashboardPanel; + timeStart: MaterialUiPickersDate; + timeEnd: MaterialUiPickersDate; + displayErrorMessage: any; } const styles = (theme: Theme) => createStyles({ ...widgetCommon, + loadingAlign: { + width: "100%", + paddingTop: "15px", + textAlign: "center", + margin: "auto", + }, }); const PieChartWidget = ({ classes, title, - pieChartConfiguration, - dataInner, - dataOuter, - middleLabel = "", + panelItem, + timeStart, + timeEnd, + displayErrorMessage, }: IPieChartWidget) => { + const [loading, setLoading] = useState(true); + const [dataInner, setDataInner] = useState([]); + const [dataOuter, setDataOuter] = useState([]); + const [result, setResult] = useState(null); + + useEffect(() => { + if (loading) { + let stepCalc = 0; + if (timeStart !== null && timeEnd !== null) { + const secondsInPeriod = timeEnd.unix() - timeStart.unix(); + const periods = Math.floor(secondsInPeriod / 60); + + stepCalc = periods < 1 ? 15 : periods; + } + + api + .invoke( + "GET", + `/api/v1/admin/info/widgets/${panelItem.id}/?step=${stepCalc}&${ + timeStart !== null ? `&start=${timeStart.unix()}` : "" + }${timeStart !== null && timeEnd !== null ? "&" : ""}${ + timeEnd !== null ? `end=${timeEnd.unix()}` : "" + }` + ) + .then((res: any) => { + const widgetsWithValue = widgetDetailsToPanel(res, panelItem); + setDataInner(widgetsWithValue.data); + setDataOuter(widgetsWithValue.dataOuter as object[]); + setResult(widgetsWithValue); + setLoading(false); + }) + .catch((err) => { + displayErrorMessage(err); + setLoading(false); + }); + } + }, [loading, panelItem, timeEnd, timeStart, displayErrorMessage]); + + const pieChartConfiguration = result + ? (result.widgetConfiguration as IPieChartConfiguration) + : []; + const middleLabel = result?.innerLabel; + const innerColors = get(pieChartConfiguration, "innerChart.colorList", []); const outerColors = get(pieChartConfiguration, "outerChart.colorList", []); return ( {title} - - - - {dataOuter && ( - - {dataOuter.map((entry, index) => ( - - ))} - - )} - {dataInner && ( - - {dataInner.map((entry, index) => { - return ( + {loading && ( + + + + )} + {!loading && ( + + + + {dataOuter && ( + + {dataOuter.map((entry, index) => ( - ); - })} - - )} - {middleLabel && ( - - {middleLabel} - - )} - - - + ))} + + )} + {dataInner && ( + + {dataInner.map((entry, index) => { + return ( + + ); + })} + + )} + {middleLabel && ( + + {middleLabel} + + )} + + + + )} ); }; -export default withStyles(styles)(PieChartWidget); +const connector = connect(null, { + displayErrorMessage: setErrorSnackMessage, +}); + +export default withStyles(styles)(connector(PieChartWidget)); diff --git a/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/SingleRepWidget.tsx b/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/SingleRepWidget.tsx index 712b98caa..09f8e8236 100644 --- a/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/SingleRepWidget.tsx +++ b/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/SingleRepWidget.tsx @@ -14,64 +14,129 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React from "react"; +import React, { useEffect, useState } from "react"; import { Area, AreaChart, ResponsiveContainer, YAxis } from "recharts"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { widgetCommon } from "../../../Common/FormComponents/common/styleLibrary"; import { IDataSRep } from "./types"; +import { connect } from "react-redux"; +import { setErrorSnackMessage } from "../../../../../actions"; +import { IDashboardPanel } from "../types"; +import { MaterialUiPickersDate } from "@material-ui/pickers/typings/date"; +import api from "../../../../../common/api"; +import { widgetDetailsToPanel } from "../utils"; +import { CircularProgress } from "@material-ui/core"; interface ISingleRepWidget { classes: any; title: string; - data: IDataSRep[]; + panelItem: IDashboardPanel; + timeStart: MaterialUiPickersDate; + timeEnd: MaterialUiPickersDate; + displayErrorMessage: any; color: string; fillColor: string; - label: string; } const styles = (theme: Theme) => createStyles({ ...widgetCommon, + loadingAlign: { + width: "100%", + paddingTop: "5px", + textAlign: "center", + margin: "auto", + }, }); const SingleRepWidget = ({ classes, title, - data, + panelItem, + timeStart, + timeEnd, + displayErrorMessage, color, fillColor, - label, }: ISingleRepWidget) => { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + const [result, setResult] = useState(null); + useEffect(() => { + if (loading) { + let stepCalc = 0; + if (timeStart !== null && timeEnd !== null) { + const secondsInPeriod = timeEnd.unix() - timeStart.unix(); + const periods = Math.floor(secondsInPeriod / 60); + + stepCalc = periods < 1 ? 15 : periods; + } + + api + .invoke( + "GET", + `/api/v1/admin/info/widgets/${panelItem.id}/?step=${stepCalc}&${ + timeStart !== null ? `&start=${timeStart.unix()}` : "" + }${timeStart !== null && timeEnd !== null ? "&" : ""}${ + timeEnd !== null ? `end=${timeEnd.unix()}` : "" + }` + ) + .then((res: any) => { + const widgetsWithValue = widgetDetailsToPanel(res, panelItem); + setResult(widgetsWithValue); + setData(widgetsWithValue.data); + setLoading(false); + }) + .catch((err) => { + displayErrorMessage(err); + setLoading(false); + }); + } + }, [loading, panelItem, timeEnd, timeStart, displayErrorMessage]); return ( {title} - - - - dataMax * 2]} hide={true} /> - - - {label} - - - - + {loading && ( + + + + )} + {!loading && ( + + + + dataMax * 2]} + hide={true} + /> + + + {result ? result.innerLabel : ""} + + + + + )} ); }; -export default withStyles(styles)(SingleRepWidget); +const connector = connect(null, { + displayErrorMessage: setErrorSnackMessage, +}); + +export default withStyles(styles)(connector(SingleRepWidget)); diff --git a/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/SingleValueWidget.tsx b/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/SingleValueWidget.tsx index 08419c037..53cd12275 100644 --- a/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/SingleValueWidget.tsx +++ b/portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/SingleValueWidget.tsx @@ -14,13 +14,23 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React from "react"; +import React, { useEffect, useState } from "react"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { widgetCommon } from "../../../Common/FormComponents/common/styleLibrary"; +import api from "../../../../../common/api"; +import { widgetDetailsToPanel } from "../utils"; +import { MaterialUiPickersDate } from "@material-ui/pickers/typings/date"; +import { IDashboardPanel } from "../types"; +import { connect } from "react-redux"; +import { setErrorSnackMessage } from "../../../../../actions"; +import { CircularProgress } from "@material-ui/core"; interface ISingleValueWidget { title: string; - data: string; + panelItem: IDashboardPanel; + timeStart: MaterialUiPickersDate; + timeEnd: MaterialUiPickersDate; + displayErrorMessage: any; classes: any; } @@ -34,15 +44,69 @@ const styles = (theme: Theme) => fontSize: 18, textAlign: "center" as const, }, + loadingAlign: { + width: "100%", + paddingTop: "15px", + textAlign: "center", + margin: "auto", + }, }); -const SingleValueWidget = ({ title, data, classes }: ISingleValueWidget) => { +const SingleValueWidget = ({ + title, + panelItem, + timeStart, + timeEnd, + displayErrorMessage, + classes, +}: ISingleValueWidget) => { + const [loading, setLoading] = useState(true); + const [data, setData] = useState(""); + useEffect(() => { + if (loading) { + let stepCalc = 0; + if (timeStart !== null && timeEnd !== null) { + const secondsInPeriod = timeEnd.unix() - timeStart.unix(); + const periods = Math.floor(secondsInPeriod / 60); + + stepCalc = periods < 1 ? 15 : periods; + } + + api + .invoke( + "GET", + `/api/v1/admin/info/widgets/${panelItem.id}/?step=${stepCalc}&${ + timeStart !== null ? `&start=${timeStart.unix()}` : "" + }${timeStart !== null && timeEnd !== null ? "&" : ""}${ + timeEnd !== null ? `end=${timeEnd.unix()}` : "" + }` + ) + .then((res: any) => { + const widgetsWithValue = widgetDetailsToPanel(res, panelItem); + setData(widgetsWithValue.data); + setLoading(false); + }) + .catch((err) => { + displayErrorMessage(err); + setLoading(false); + }); + } + }, [loading, panelItem, timeEnd, timeStart, displayErrorMessage]); return ( {title} - {data} + {loading && ( + + + + )} + {!loading && {data}} ); }; -export default withStyles(styles)(SingleValueWidget); +const connector = connect(null, { + displayErrorMessage: setErrorSnackMessage, +}); + +export default withStyles(styles)(connector(SingleValueWidget)); diff --git a/portal-ui/src/screens/Console/Dashboard/Prometheus/types.ts b/portal-ui/src/screens/Console/Dashboard/Prometheus/types.ts index 82dbf03c3..a113ea500 100644 --- a/portal-ui/src/screens/Console/Dashboard/Prometheus/types.ts +++ b/portal-ui/src/screens/Console/Dashboard/Prometheus/types.ts @@ -31,6 +31,7 @@ export enum widgetType { } export interface IDashboardPanel { + id: number; title: string; data: string | object[] | IDataSRep[]; dataOuter?: string | object[]; diff --git a/portal-ui/src/screens/Console/Dashboard/Prometheus/utils.ts b/portal-ui/src/screens/Console/Dashboard/Prometheus/utils.ts index 7c56e1b01..9cecd30d7 100644 --- a/portal-ui/src/screens/Console/Dashboard/Prometheus/utils.ts +++ b/portal-ui/src/screens/Console/Dashboard/Prometheus/utils.ts @@ -331,6 +331,7 @@ const roundNumber = (value: string) => { export const panelsConfiguration: IDashboardPanel[] = [ { + id: 1, title: "Uptime", data: "N/A", type: widgetType.singleValue, @@ -338,18 +339,21 @@ export const panelsConfiguration: IDashboardPanel[] = [ labelDisplayFunction: niceDays, }, { + id: 9, title: "Total Online Disks", data: "N/A", type: widgetType.singleValue, layoutIdentifier: "panel-1", }, { + id: 78, title: "Total Offline Disks", data: "N/A", type: widgetType.singleValue, layoutIdentifier: "panel-2", }, { + id: 50, title: "Current Usable Capacity", data: [], dataOuter: [{ name: "outer", value: 100 }], @@ -375,6 +379,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ labelDisplayFunction: niceBytes, }, { + id: 68, title: "Data Usage Growth", data: [], widgetConfiguration: [ @@ -391,6 +396,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ xAxisFormatter: getTimeFromTimestamp, }, { + id: 52, title: "Object size distribution", data: [], widgetConfiguration: [ @@ -433,18 +439,21 @@ export const panelsConfiguration: IDashboardPanel[] = [ layoutIdentifier: "panel-5", }, { + id: 53, title: "Total Online Servers", data: "N/A", type: widgetType.singleValue, layoutIdentifier: "panel-6", }, { + id: 69, title: "Total Offline Servers", data: "N/A", type: widgetType.singleValue, layoutIdentifier: "panel-7", }, { + id: 66, title: "Number of Buckets", data: [], innerLabel: "N/A", @@ -454,6 +463,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ layoutIdentifier: "panel-8", }, { + id: 44, title: "Number of Objects", data: [], innerLabel: "N/A", @@ -463,6 +473,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ layoutIdentifier: "panel-9", }, { + id: 63, title: "S3 API Data Received Rate", data: [], widgetConfiguration: [ @@ -479,6 +490,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ yAxisFormatter: niceBytes, }, { + id: 61, title: "Total Open FDs", data: [], innerLabel: "N/A", @@ -488,6 +500,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ fillColor: "#A6E8C4", }, { + id: 62, title: "Total Goroutines", data: [], innerLabel: "N/A", @@ -497,6 +510,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ fillColor: "#F4CECE", }, { + id: 77, title: "Node CPU Usage", data: [], widgetConfiguration: [ @@ -513,6 +527,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ xAxisFormatter: getTimeFromTimestamp, }, { + id: 60, title: "S3 API Request Rate", data: [], widgetConfiguration: [ @@ -528,6 +543,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ xAxisFormatter: getTimeFromTimestamp, }, { + id: 70, title: "S3 API Data Sent Rate", data: [], widgetConfiguration: [ @@ -544,6 +560,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ yAxisFormatter: niceBytes, }, { + id: 17, title: "Internode Data Transfer", data: [], widgetConfiguration: [ @@ -560,6 +577,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ xAxisFormatter: getTimeFromTimestamp, }, { + id: 73, title: "Node IO", data: [], widgetConfiguration: [ @@ -576,6 +594,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ xAxisFormatter: getTimeFromTimestamp, }, { + id: 80, title: "Time Since Last Heal Activity", data: "N/A", type: widgetType.singleValue, @@ -583,6 +602,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ labelDisplayFunction: niceDaysFromNS, }, { + id: 81, title: "Time Since Last Scan Activity", data: "N/A", type: widgetType.singleValue, @@ -590,6 +610,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ labelDisplayFunction: niceDaysFromNS, }, { + id: 71, title: "S3 API Request Error Rate", data: [], widgetConfiguration: [ @@ -605,6 +626,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ xAxisFormatter: getTimeFromTimestamp, }, { + id: 76, title: "Node Memory Usage", data: [], widgetConfiguration: [ @@ -621,6 +643,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ yAxisFormatter: niceBytes, }, { + id: 74, title: "Drive Used Capacity", data: [], widgetConfiguration: [ @@ -637,6 +660,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ yAxisFormatter: niceBytes, }, { + id: 82, title: "Drives Free Inodes", data: [], widgetConfiguration: [ @@ -653,6 +677,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ xAxisFormatter: getTimeFromTimestamp, }, { + id: 11, title: "Node Syscalls", data: [], widgetConfiguration: [ @@ -669,6 +694,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ xAxisFormatter: getTimeFromTimestamp, }, { + id: 8, title: "Node File Descriptors", data: [], widgetConfiguration: [ @@ -685,6 +711,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ xAxisFormatter: getTimeFromTimestamp, }, { + id: 65, title: "Total S3 Traffic Inbound", data: "N/A", type: widgetType.singleValue, @@ -692,6 +719,7 @@ export const panelsConfiguration: IDashboardPanel[] = [ labelDisplayFunction: niceBytes, }, { + id: 64, title: "Total S3 Traffic Outbound", data: "N/A", type: widgetType.singleValue, @@ -737,297 +765,303 @@ const constructLabelNames = (metrics: any, legendFormat: string) => { let cleanLegend = replacedLegend.replace(/{{(.*?)}}/g, ""); - if (countVarsOpen === countVarsClose && countVarsOpen !== 0 && countVarsClose !== 0) { - + if ( + countVarsOpen === countVarsClose && + countVarsOpen !== 0 && + countVarsClose !== 0 + ) { keysToReplace.forEach((element) => { replacedLegend = replacedLegend.replace(element, metrics[element]); - }) + }); cleanLegend = replacedLegend; } - + // In case not all the legends were replaced, we remove the placeholders. return cleanLegend; }; -export const getWidgetsWithValue = (payload: any[]) => { - return panelsConfiguration.map((panelItem) => { +export const getWidgetsWithValue = (payload: any[]): IDashboardPanel[] => { + return panelsConfiguration.map((panelItem: IDashboardPanel) => { const payloadData = payload.find( (panelT) => panelT.title.toLowerCase().trim() === panelItem.title.toLowerCase().trim() ); + return widgetDetailsToPanel(payloadData, panelItem); + }); +}; - if (!payloadData) { - return panelItem; - } +export const widgetDetailsToPanel = ( + payloadData: any, + panelItem: IDashboardPanel +) => { + if (!payloadData) { + return panelItem; + } - const typeOfPayload = payloadData.type; + const typeOfPayload = payloadData.type; - switch (panelItem.type) { - case widgetType.singleValue: - if (typeOfPayload === "stat" || typeOfPayload === "singlestat") { - // We sort values & get the last value - let elements = get(payloadData, "targets[0].result[0].values", []); + switch (panelItem.type) { + case widgetType.singleValue: + if (typeOfPayload === "stat" || typeOfPayload === "singlestat") { + // We sort values & get the last value + let elements = get(payloadData, "targets[0].result[0].values", []); - if (elements === null) { - elements = []; - } - - const metricCalc = get( - payloadData, - "options.reduceOptions.calcs[0]", - "lastNotNull" - ); - - const valueDisplay = calculateMainValue(elements, metricCalc); - - const data = panelItem.labelDisplayFunction - ? panelItem.labelDisplayFunction(valueDisplay[1]) - : valueDisplay[1]; - - return { - ...panelItem, - data, - }; + if (elements === null) { + elements = []; } - break; - case widgetType.pieChart: - if (typeOfPayload === "gauge") { - let chartSeries = get(payloadData, "targets[0].result", []); - if (chartSeries === null) { - chartSeries = []; - } + const metricCalc = get( + payloadData, + "options.reduceOptions.calcs[0]", + "lastNotNull" + ); - const metricCalc = get( - payloadData, - "options.reduceOptions.calcs[0]", - "lastNotNull" - ); + const valueDisplay = calculateMainValue(elements, metricCalc); - const valuesArray = - chartSeries.length > 0 ? chartSeries[0].values : []; + const data = panelItem.labelDisplayFunction + ? panelItem.labelDisplayFunction(valueDisplay[1]) + : valueDisplay[1]; - const totalValues = calculateMainValue(valuesArray, metricCalc); + return { + ...panelItem, + data, + }; + } + break; + case widgetType.pieChart: + if (typeOfPayload === "gauge") { + let chartSeries = get(payloadData, "targets[0].result", []); - const values = chartSeries.map((elementValue: any) => { - const values = get(elementValue, "values", []); - const metricKeyItem = Object.keys(elementValue.metric); - - const sortResult = values.sort( - (value1: any[], value2: any[]) => value1[0] - value2[0] - ); - - const metricName = elementValue.metric[metricKeyItem[0]]; - const value = sortResult[sortResult.length - 1]; - return { name: metricName, value: parseInt(value) }; - }); - - const innerLabel = panelItem.labelDisplayFunction - ? panelItem.labelDisplayFunction(totalValues[1]) - : totalValues[1]; - - return { - ...panelItem, - data: values, - innerLabel, - }; + if (chartSeries === null) { + chartSeries = []; } - break; - case widgetType.linearGraph: - if (typeOfPayload === "graph") { - let targets = get(payloadData, "targets", []); - if (targets === null) { - targets = []; - } - const series: any[] = []; - const plotValues: any[] = []; + const metricCalc = get( + payloadData, + "options.reduceOptions.calcs[0]", + "lastNotNull" + ); - targets.forEach( - ( - targetMaster: { legendFormat: string; result: any[] }, - index: number - ) => { - // Add a new serie to plot variables in case it is not from multiple values - let results = get(targetMaster, "result", []); - const legendFormat = targetMaster.legendFormat; - if (results === null) { - results = []; - } + const valuesArray = chartSeries.length > 0 ? chartSeries[0].values : []; - results.forEach((itemVals: { metric: object; values: any[] }) => { - // Label Creation - const labelName = constructLabelNames( - itemVals.metric, - legendFormat - ); - const keyName = `key_${index}${labelName}`; + const totalValues = calculateMainValue(valuesArray, metricCalc); - // series creation with recently created label - series.push({ - dataKey: keyName, - keyLabel: labelName, - lineColor: "", - fillColor: "", - }); + const values = chartSeries.map((elementValue: any) => { + const values = get(elementValue, "values", []); + const metricKeyItem = Object.keys(elementValue.metric); - // we iterate over values and create elements - let values = get(itemVals, "values", []); - if (values === null) { - values = []; - } - - values.forEach((valInfo: any[]) => { - const itemIndex = plotValues.findIndex( - (element) => element.name === valInfo[0] - ); - - // Element not exists yet - if (itemIndex === -1) { - let itemToPush: any = { name: valInfo[0] }; - itemToPush[keyName] = valInfo[1]; - - plotValues.push(itemToPush); - } else { - plotValues[itemIndex][keyName] = valInfo[1]; - } - }); - }); - } - ); - - const sortedSeries = series.sort((series1: any, series2: any) => { - if (series1.keyLabel < series2.keyLabel) { - return -1; - } - if (series1.keyLabel > series2.keyLabel) { - return 1; - } - return 0; - }); - - const seriesWithColors = sortedSeries.map( - (serialC: any, index: number) => { - return { - ...serialC, - lineColor: - colorsMain[index] || textToRGBColor(serialC.keyLabel), - fillColor: - colorsMain[index] || textToRGBColor(serialC.keyLabel), - }; - } - ); - - const sortedVals = plotValues.sort( - (value1: any, value2: any) => value1.name - value2.name - ); - - return { - ...panelItem, - widgetConfiguration: seriesWithColors, - data: sortedVals, - }; - } - break; - case widgetType.barChart: - if (typeOfPayload === "bargauge") { - let chartBars = get(payloadData, "targets[0].result", []); - - if (chartBars === null) { - chartBars = []; - } - - const sortFunction = (value1: any[], value2: any[]) => - value1[0] - value2[0]; - - let values = []; - if (panelItem.customStructure) { - values = panelItem.customStructure.map((structureItem) => { - const metricTake = chartBars.find((element: any) => { - const metricKeyItem = Object.keys(element.metric); - - const metricName = element.metric[metricKeyItem[0]]; - - return metricName === structureItem.originTag; - }); - - const elements = get(metricTake, "values", []); - - const sortResult = elements.sort(sortFunction); - const lastValue = sortResult[sortResult.length - 1] || ["", "0"]; - - return { - name: structureItem.displayTag, - a: parseInt(lastValue[1]), - }; - }); - } else { - // If no configuration is set, we construct the series for bar chart and return the element - values = chartBars.map((elementValue: any) => { - const metricKeyItem = Object.keys(elementValue.metric); - - const metricName = elementValue.metric[metricKeyItem[0]]; - - const elements = get(elementValue, "values", []); - - const sortResult = elements.sort(sortFunction); - const lastValue = sortResult[sortResult.length - 1] || ["", "0"]; - return { name: metricName, a: parseInt(lastValue[1]) }; - }); - } - - return { - ...panelItem, - data: values, - }; - } - break; - case widgetType.singleRep: - if (typeOfPayload === "stat") { - // We sort values & get the last value - let elements = get(payloadData, "targets[0].result[0].values", []); - if (elements === null) { - elements = []; - } - const metricCalc = get( - payloadData, - "options.reduceOptions.calcs[0]", - "lastNotNull" - ); - - const valueDisplay = calculateMainValue(elements, metricCalc); - - const sortResult = elements.sort( + const sortResult = values.sort( (value1: any[], value2: any[]) => value1[0] - value2[0] ); - let valuesForBackground = []; + const metricName = elementValue.metric[metricKeyItem[0]]; + const value = sortResult[sortResult.length - 1]; + return { name: metricName, value: parseInt(value) }; + }); - if (sortResult.length === 1) { - valuesForBackground.push({ value: 0 }); - } + const innerLabel = panelItem.labelDisplayFunction + ? panelItem.labelDisplayFunction(totalValues[1]) + : totalValues[1]; - sortResult.forEach((eachVal: any) => { - valuesForBackground.push({ value: parseInt(eachVal[1]) }); - }); - - const innerLabel = panelItem.labelDisplayFunction - ? panelItem.labelDisplayFunction(valueDisplay[1]) - : valueDisplay[1]; - - return { - ...panelItem, - data: valuesForBackground, - innerLabel, - }; + return { + ...panelItem, + data: values, + innerLabel, + }; + } + break; + case widgetType.linearGraph: + if (typeOfPayload === "graph") { + let targets = get(payloadData, "targets", []); + if (targets === null) { + targets = []; } - break; - } - return panelItem; - }); + const series: any[] = []; + const plotValues: any[] = []; + + targets.forEach( + ( + targetMaster: { legendFormat: string; result: any[] }, + index: number + ) => { + // Add a new serie to plot variables in case it is not from multiple values + let results = get(targetMaster, "result", []); + const legendFormat = targetMaster.legendFormat; + if (results === null) { + results = []; + } + + results.forEach((itemVals: { metric: object; values: any[] }) => { + // Label Creation + const labelName = constructLabelNames( + itemVals.metric, + legendFormat + ); + const keyName = `key_${index}${labelName}`; + + // series creation with recently created label + series.push({ + dataKey: keyName, + keyLabel: labelName, + lineColor: "", + fillColor: "", + }); + + // we iterate over values and create elements + let values = get(itemVals, "values", []); + if (values === null) { + values = []; + } + + values.forEach((valInfo: any[]) => { + const itemIndex = plotValues.findIndex( + (element) => element.name === valInfo[0] + ); + + // Element not exists yet + if (itemIndex === -1) { + let itemToPush: any = { name: valInfo[0] }; + itemToPush[keyName] = valInfo[1]; + + plotValues.push(itemToPush); + } else { + plotValues[itemIndex][keyName] = valInfo[1]; + } + }); + }); + } + ); + + const sortedSeries = series.sort((series1: any, series2: any) => { + if (series1.keyLabel < series2.keyLabel) { + return -1; + } + if (series1.keyLabel > series2.keyLabel) { + return 1; + } + return 0; + }); + + const seriesWithColors = sortedSeries.map( + (serialC: any, index: number) => { + return { + ...serialC, + lineColor: colorsMain[index] || textToRGBColor(serialC.keyLabel), + fillColor: colorsMain[index] || textToRGBColor(serialC.keyLabel), + }; + } + ); + + const sortedVals = plotValues.sort( + (value1: any, value2: any) => value1.name - value2.name + ); + + return { + ...panelItem, + widgetConfiguration: seriesWithColors, + data: sortedVals, + }; + } + break; + case widgetType.barChart: + if (typeOfPayload === "bargauge") { + let chartBars = get(payloadData, "targets[0].result", []); + + if (chartBars === null) { + chartBars = []; + } + + const sortFunction = (value1: any[], value2: any[]) => + value1[0] - value2[0]; + + let values = []; + if (panelItem.customStructure) { + values = panelItem.customStructure.map((structureItem) => { + const metricTake = chartBars.find((element: any) => { + const metricKeyItem = Object.keys(element.metric); + + const metricName = element.metric[metricKeyItem[0]]; + + return metricName === structureItem.originTag; + }); + + const elements = get(metricTake, "values", []); + + const sortResult = elements.sort(sortFunction); + const lastValue = sortResult[sortResult.length - 1] || ["", "0"]; + + return { + name: structureItem.displayTag, + a: parseInt(lastValue[1]), + }; + }); + } else { + // If no configuration is set, we construct the series for bar chart and return the element + values = chartBars.map((elementValue: any) => { + const metricKeyItem = Object.keys(elementValue.metric); + + const metricName = elementValue.metric[metricKeyItem[0]]; + + const elements = get(elementValue, "values", []); + + const sortResult = elements.sort(sortFunction); + const lastValue = sortResult[sortResult.length - 1] || ["", "0"]; + return { name: metricName, a: parseInt(lastValue[1]) }; + }); + } + + return { + ...panelItem, + data: values, + }; + } + break; + case widgetType.singleRep: + if (typeOfPayload === "stat") { + // We sort values & get the last value + let elements = get(payloadData, "targets[0].result[0].values", []); + if (elements === null) { + elements = []; + } + const metricCalc = get( + payloadData, + "options.reduceOptions.calcs[0]", + "lastNotNull" + ); + + const valueDisplay = calculateMainValue(elements, metricCalc); + + const sortResult = elements.sort( + (value1: any[], value2: any[]) => value1[0] - value2[0] + ); + + let valuesForBackground = []; + + if (sortResult.length === 1) { + valuesForBackground.push({ value: 0 }); + } + + sortResult.forEach((eachVal: any) => { + valuesForBackground.push({ value: parseInt(eachVal[1]) }); + }); + + const innerLabel = panelItem.labelDisplayFunction + ? panelItem.labelDisplayFunction(valueDisplay[1]) + : valueDisplay[1]; + + return { + ...panelItem, + data: valuesForBackground, + innerLabel, + }; + } + break; + } + + return panelItem; }; export const saveDashboardDistribution = (configuration: Layout[]) => { diff --git a/restapi/admin_info.go b/restapi/admin_info.go index 08dcb6725..1f9b34dfe 100644 --- a/restapi/admin_info.go +++ b/restapi/admin_info.go @@ -28,6 +28,8 @@ import ( "strings" "time" + "github.com/go-openapi/swag" + "github.com/go-openapi/runtime/middleware" "github.com/minio/console/models" "github.com/minio/console/restapi/operations" @@ -37,12 +39,20 @@ import ( func registerAdminInfoHandlers(api *operations.ConsoleAPI) { // return usage stats api.AdminAPIAdminInfoHandler = admin_api.AdminInfoHandlerFunc(func(params admin_api.AdminInfoParams, session *models.Principal) middleware.Responder { - infoResp, err := getAdminInfoResponse(session, params) + infoResp, err := getAdminInfoResponse(session) if err != nil { return admin_api.NewAdminInfoDefault(int(err.Code)).WithPayload(err) } return admin_api.NewAdminInfoOK().WithPayload(infoResp) }) + // return single widget results + api.AdminAPIDashboardWidgetDetailsHandler = admin_api.DashboardWidgetDetailsHandlerFunc(func(params admin_api.DashboardWidgetDetailsParams, session *models.Principal) middleware.Responder { + infoResp, err := getAdminInfoWidgetResponse(params) + if err != nil { + return admin_api.NewDashboardWidgetDetailsDefault(int(err.Code)).WithPayload(err) + } + return admin_api.NewDashboardWidgetDetailsOK().WithPayload(infoResp) + }) } @@ -779,7 +789,7 @@ type LabelResults struct { var jobRegex = regexp.MustCompile(`(?m)\{[a-z]+\=\".*?\"\}`) // getAdminInfoResponse returns the response containing total buckets, objects and usage. -func getAdminInfoResponse(session *models.Principal, params admin_api.AdminInfoParams) (*models.AdminInfoResponse, *models.Error) { +func getAdminInfoResponse(session *models.Principal) (*models.AdminInfoResponse, *models.Error) { prometheusURL := getPrometheusURL() if prometheusURL == "" { @@ -806,6 +816,37 @@ func getAdminInfoResponse(session *models.Principal, params admin_api.AdminInfoP return sessionResp, nil } + var wdgts []*models.Widget + + for _, m := range widgets { + // for each target we will launch another goroutine to fetch the values + wdgtResult := models.Widget{ + ID: m.ID, + Title: m.Title, + Type: m.Type, + } + if len(m.Options.ReduceOptions.Calcs) > 0 { + wdgtResult.Options = &models.WidgetOptions{ + ReduceOptions: &models.WidgetOptionsReduceOptions{ + Calcs: m.Options.ReduceOptions.Calcs, + }, + } + } + + wdgts = append(wdgts, &wdgtResult) + } + + // count the number of widgets that have completed calculating + sessionResp := &models.AdminInfoResponse{} + + sessionResp.Widgets = wdgts + + return sessionResp, nil +} + +func getAdminInfoWidgetResponse(params admin_api.DashboardWidgetDetailsParams) (*models.WidgetDetails, *models.Error) { + prometheusURL := getPrometheusURL() + labelResultsCh := make(chan LabelResults) for _, lbl := range labels { @@ -865,160 +906,139 @@ LabelsWaitLoop: // launch a goroutines per widget - results := make(chan models.Widget) for _, m := range widgets { - go func(m Metric, params admin_api.AdminInfoParams) { - targetResults := make(chan *models.ResultTarget) - // for each target we will launch another goroutine to fetch the values - for _, target := range m.Targets { - go func(target Target, params admin_api.AdminInfoParams) { - apiType := "query_range" - now := time.Now() + if m.ID != params.WidgetID { + continue + } - extraParamters := fmt.Sprintf("&start=%d&end=%d", now.Add(-15*time.Minute).Unix(), now.Unix()) + targetResults := make(chan *models.ResultTarget) + // for each target we will launch another goroutine to fetch the values + for _, target := range m.Targets { + go func(target Target, params admin_api.DashboardWidgetDetailsParams) { + apiType := "query_range" + now := time.Now() - var step int32 = 60 - if target.Step > 0 { - step = target.Step - } - if params.Step != nil && *params.Step > 0 { - step = *params.Step - } - if step > 0 { - extraParamters = fmt.Sprintf("%s&step=%d", extraParamters, step) - } + extraParamters := fmt.Sprintf("&start=%d&end=%d", now.Add(-15*time.Minute).Unix(), now.Unix()) - if params.Start != nil && params.End != nil { - extraParamters = fmt.Sprintf("&start=%d&end=%d&step=%d", *params.Start, *params.End, *params.Step) - } + var step int32 = 60 + if target.Step > 0 { + step = target.Step + } + if params.Step != nil && *params.Step > 0 { + step = *params.Step + } + if step > 0 { + extraParamters = fmt.Sprintf("%s&step=%d", extraParamters, step) + } - // replace the `$__interval` global for step with unit (s for seconds) - queryExpr := strings.ReplaceAll(target.Expr, "$__interval", fmt.Sprintf("%ds", 120)) - if strings.Contains(queryExpr, "$") { - var re = regexp.MustCompile(`\$([a-z]+)`) + if params.Start != nil && params.End != nil { + extraParamters = fmt.Sprintf("&start=%d&end=%d&step=%d", *params.Start, *params.End, *params.Step) + } - for _, match := range re.FindAllStringSubmatch(queryExpr, -1) { - if val, ok := labelMap[match[1]]; ok { - queryExpr = strings.ReplaceAll(queryExpr, "$"+match[1], fmt.Sprintf("(%s)", strings.Join(val, "|"))) - } + // replace the `$__interval` global for step with unit (s for seconds) + queryExpr := strings.ReplaceAll(target.Expr, "$__interval", fmt.Sprintf("%ds", 120)) + if strings.Contains(queryExpr, "$") { + var re = regexp.MustCompile(`\$([a-z]+)`) + + for _, match := range re.FindAllStringSubmatch(queryExpr, -1) { + if val, ok := labelMap[match[1]]; ok { + queryExpr = strings.ReplaceAll(queryExpr, "$"+match[1], fmt.Sprintf("(%s)", strings.Join(val, "|"))) } } + } - // replace the weird {job="asd"} in the exp - if strings.Contains(queryExpr, "job=") { - queryExpr = jobRegex.ReplaceAllString(queryExpr, "") + // replace the weird {job="asd"} in the exp + if strings.Contains(queryExpr, "job=") { + queryExpr = jobRegex.ReplaceAllString(queryExpr, "") + } + + endpoint := fmt.Sprintf("%s/api/v1/%s?query=%s%s", getPrometheusURL(), apiType, url.QueryEscape(queryExpr), extraParamters) + + resp, err := http.Get(endpoint) + if err != nil { + log.Println(err) + return + } + defer func() { + if err := resp.Body.Close(); err != nil { + log.Println(err) } + }() - endpoint := fmt.Sprintf("%s/api/v1/%s?query=%s%s", getPrometheusURL(), apiType, url.QueryEscape(queryExpr), extraParamters) - - resp, err := http.Get(endpoint) + if resp.StatusCode != 200 { + body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Println(err) return } - defer func() { - if err := resp.Body.Close(); err != nil { - log.Println(err) - } - }() - - if resp.StatusCode != 200 { - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - log.Println(err) - return - } - log.Println(endpoint) - log.Println(resp.StatusCode) - log.Println(string(body)) - return - } - - var response PromResp - jd := json.NewDecoder(resp.Body) - if err = jd.Decode(&response); err != nil { - log.Println(err) - return - } - //body, _ := ioutil.ReadAll(resp.Body) - //err = json.Unmarshal(body, &response) - //if err != nil { - // log.Println(err) - //} - - targetResult := models.ResultTarget{ - LegendFormat: target.LegendFormat, - ResultType: response.Data.ResultType, - } - for _, r := range response.Data.Result { - targetResult.Result = append(targetResult.Result, &models.WidgetResult{ - Metric: r.Metric, - Values: r.Values, - }) - } - - //xx, err := json.Marshal(response) - //if err != nil { - // log.Println(err) - //} - //log.Println("----", m.Title) - //log.Println(string(body)) - //log.Println(string(xx)) - //log.Println("=====") - - targetResults <- &targetResult - - }(target, params) - } - - wdgtResult := models.Widget{ - Title: m.Title, - Type: m.Type, - } - if len(m.Options.ReduceOptions.Calcs) > 0 { - wdgtResult.Options = &models.WidgetOptions{ - ReduceOptions: &models.WidgetOptionsReduceOptions{ - Calcs: m.Options.ReduceOptions.Calcs, - }, + log.Println(endpoint) + log.Println(resp.StatusCode) + log.Println(string(body)) + return } - } - // count how many targets we have received - targetsReceived := 0 - for res := range targetResults { - wdgtResult.Targets = append(wdgtResult.Targets, res) - targetsReceived++ - // upon receiving the total number of targets needed, we can close the channel to not lock the goroutine - if targetsReceived >= len(m.Targets) { - close(targetResults) + var response PromResp + jd := json.NewDecoder(resp.Body) + if err = jd.Decode(&response); err != nil { + log.Println(err) + return } - } + //body, _ := ioutil.ReadAll(resp.Body) + //err = json.Unmarshal(body, &response) + //if err != nil { + // log.Println(err) + //} - results <- wdgtResult - }(m, params) - } + targetResult := models.ResultTarget{ + LegendFormat: target.LegendFormat, + ResultType: response.Data.ResultType, + } + for _, r := range response.Data.Result { + targetResult.Result = append(targetResult.Result, &models.WidgetResult{ + Metric: r.Metric, + Values: r.Values, + }) + } - // count the number of widgets that have completed calculating - totalWidgets := 0 - sessionResp := &models.AdminInfoResponse{} + //xx, err := json.Marshal(response) + //if err != nil { + // log.Println(err) + //} + //log.Println("----", m.Title) + //log.Println(string(body)) + //log.Println(string(xx)) + //log.Println("=====") - var wdgts []*models.Widget - // wait for as many goroutines that come back in less than 1 second -WaitLoop: - for { - select { - case <-time.After(1 * time.Second): - break WaitLoop - case res := <-results: - wdgts = append(wdgts, &res) - totalWidgets++ - if totalWidgets >= len(widgets) { - break WaitLoop + targetResults <- &targetResult + + }(target, params) + } + + wdgtResult := models.WidgetDetails{ + ID: m.ID, + Title: m.Title, + Type: m.Type, + } + if len(m.Options.ReduceOptions.Calcs) > 0 { + wdgtResult.Options = &models.WidgetDetailsOptions{ + ReduceOptions: &models.WidgetDetailsOptionsReduceOptions{ + Calcs: m.Options.ReduceOptions.Calcs, + }, } } + // count how many targets we have received + targetsReceived := 0 + + for res := range targetResults { + wdgtResult.Targets = append(wdgtResult.Targets, res) + targetsReceived++ + // upon receiving the total number of targets needed, we can close the channel to not lock the goroutine + if targetsReceived >= len(m.Targets) { + close(targetResults) + } + } + return &wdgtResult, nil } - sessionResp.Widgets = wdgts - - return sessionResp, nil + return nil, &models.Error{Code: 404, Message: swag.String("Widget not found")} } diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index 54469383d..9f95aa530 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -115,7 +115,37 @@ func init() { ], "summary": "Returns information about the deployment", "operationId": "AdminInfo", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/adminInfoResponse" + } + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, + "/admin/info/widgets/{widgetId}": { + "get": { + "tags": [ + "AdminAPI" + ], + "summary": "Returns information about the deployment", + "operationId": "DashboardWidgetDetails", "parameters": [ + { + "type": "integer", + "format": "int32", + "name": "widgetId", + "in": "path", + "required": true + }, { "type": "integer", "name": "start", @@ -137,7 +167,7 @@ func init() { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/adminInfoResponse" + "$ref": "#/definitions/widgetDetails" } }, "default": { @@ -6960,6 +6990,47 @@ func init() { "widget": { "type": "object", "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "options": { + "type": "object", + "properties": { + "reduceOptions": { + "type": "object", + "properties": { + "calcs": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "targets": { + "type": "array", + "items": { + "$ref": "#/definitions/resultTarget" + } + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "widgetDetails": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, "options": { "type": "object", "properties": { @@ -7101,7 +7172,37 @@ func init() { ], "summary": "Returns information about the deployment", "operationId": "AdminInfo", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/adminInfoResponse" + } + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, + "/admin/info/widgets/{widgetId}": { + "get": { + "tags": [ + "AdminAPI" + ], + "summary": "Returns information about the deployment", + "operationId": "DashboardWidgetDetails", "parameters": [ + { + "type": "integer", + "format": "int32", + "name": "widgetId", + "in": "path", + "required": true + }, { "type": "integer", "name": "start", @@ -7123,7 +7224,7 @@ func init() { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/adminInfoResponse" + "$ref": "#/definitions/widgetDetails" } }, "default": { @@ -11440,6 +11541,33 @@ func init() { } } }, + "WidgetDetailsOptions": { + "type": "object", + "properties": { + "reduceOptions": { + "type": "object", + "properties": { + "calcs": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "WidgetDetailsOptionsReduceOptions": { + "type": "object", + "properties": { + "calcs": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "WidgetOptions": { "type": "object", "properties": { @@ -14412,6 +14540,47 @@ func init() { "widget": { "type": "object", "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "options": { + "type": "object", + "properties": { + "reduceOptions": { + "type": "object", + "properties": { + "calcs": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "targets": { + "type": "array", + "items": { + "$ref": "#/definitions/resultTarget" + } + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "widgetDetails": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, "options": { "type": "object", "properties": { diff --git a/restapi/operations/admin_api/admin_info_parameters.go b/restapi/operations/admin_api/admin_info_parameters.go index b072e6bc5..25398d3c7 100644 --- a/restapi/operations/admin_api/admin_info_parameters.go +++ b/restapi/operations/admin_api/admin_info_parameters.go @@ -26,10 +26,7 @@ import ( "net/http" "github.com/go-openapi/errors" - "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" - "github.com/go-openapi/strfmt" - "github.com/go-openapi/swag" ) // NewAdminInfoParams creates a new AdminInfoParams object @@ -47,19 +44,6 @@ type AdminInfoParams struct { // HTTP Request Object HTTPRequest *http.Request `json:"-"` - - /* - In: query - */ - End *int64 - /* - In: query - */ - Start *int64 - /* - In: query - */ - Step *int32 } // BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface @@ -71,91 +55,8 @@ func (o *AdminInfoParams) BindRequest(r *http.Request, route *middleware.Matched o.HTTPRequest = r - qs := runtime.Values(r.URL.Query()) - - qEnd, qhkEnd, _ := qs.GetOK("end") - if err := o.bindEnd(qEnd, qhkEnd, route.Formats); err != nil { - res = append(res, err) - } - - qStart, qhkStart, _ := qs.GetOK("start") - if err := o.bindStart(qStart, qhkStart, route.Formats); err != nil { - res = append(res, err) - } - - qStep, qhkStep, _ := qs.GetOK("step") - if err := o.bindStep(qStep, qhkStep, route.Formats); err != nil { - res = append(res, err) - } - if len(res) > 0 { return errors.CompositeValidationError(res...) } return nil } - -// bindEnd binds and validates parameter End from query. -func (o *AdminInfoParams) bindEnd(rawData []string, hasKey bool, formats strfmt.Registry) error { - var raw string - if len(rawData) > 0 { - raw = rawData[len(rawData)-1] - } - - // Required: false - // AllowEmptyValue: false - if raw == "" { // empty values pass all other validations - return nil - } - - value, err := swag.ConvertInt64(raw) - if err != nil { - return errors.InvalidType("end", "query", "int64", raw) - } - o.End = &value - - return nil -} - -// bindStart binds and validates parameter Start from query. -func (o *AdminInfoParams) bindStart(rawData []string, hasKey bool, formats strfmt.Registry) error { - var raw string - if len(rawData) > 0 { - raw = rawData[len(rawData)-1] - } - - // Required: false - // AllowEmptyValue: false - if raw == "" { // empty values pass all other validations - return nil - } - - value, err := swag.ConvertInt64(raw) - if err != nil { - return errors.InvalidType("start", "query", "int64", raw) - } - o.Start = &value - - return nil -} - -// bindStep binds and validates parameter Step from query. -func (o *AdminInfoParams) bindStep(rawData []string, hasKey bool, formats strfmt.Registry) error { - var raw string - if len(rawData) > 0 { - raw = rawData[len(rawData)-1] - } - - // Required: false - // AllowEmptyValue: false - if raw == "" { // empty values pass all other validations - return nil - } - - value, err := swag.ConvertInt32(raw) - if err != nil { - return errors.InvalidType("step", "query", "int32", raw) - } - o.Step = &value - - return nil -} diff --git a/restapi/operations/admin_api/admin_info_urlbuilder.go b/restapi/operations/admin_api/admin_info_urlbuilder.go index 07806dca9..ea538d8e4 100644 --- a/restapi/operations/admin_api/admin_info_urlbuilder.go +++ b/restapi/operations/admin_api/admin_info_urlbuilder.go @@ -26,19 +26,11 @@ import ( "errors" "net/url" golangswaggerpaths "path" - - "github.com/go-openapi/swag" ) // AdminInfoURL generates an URL for the admin info operation type AdminInfoURL struct { - End *int64 - Start *int64 - Step *int32 - _basePath string - // avoid unkeyed usage - _ struct{} } // WithBasePath sets the base path for this url builder, only required when it's different from the @@ -68,34 +60,6 @@ func (o *AdminInfoURL) Build() (*url.URL, error) { } _result.Path = golangswaggerpaths.Join(_basePath, _path) - qs := make(url.Values) - - var endQ string - if o.End != nil { - endQ = swag.FormatInt64(*o.End) - } - if endQ != "" { - qs.Set("end", endQ) - } - - var startQ string - if o.Start != nil { - startQ = swag.FormatInt64(*o.Start) - } - if startQ != "" { - qs.Set("start", startQ) - } - - var stepQ string - if o.Step != nil { - stepQ = swag.FormatInt32(*o.Step) - } - if stepQ != "" { - qs.Set("step", stepQ) - } - - _result.RawQuery = qs.Encode() - return &_result, nil } diff --git a/restapi/operations/admin_api/dashboard_widget_details.go b/restapi/operations/admin_api/dashboard_widget_details.go new file mode 100644 index 000000000..681b339d7 --- /dev/null +++ b/restapi/operations/admin_api/dashboard_widget_details.go @@ -0,0 +1,90 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" + + "github.com/minio/console/models" +) + +// DashboardWidgetDetailsHandlerFunc turns a function with the right signature into a dashboard widget details handler +type DashboardWidgetDetailsHandlerFunc func(DashboardWidgetDetailsParams, *models.Principal) middleware.Responder + +// Handle executing the request and returning a response +func (fn DashboardWidgetDetailsHandlerFunc) Handle(params DashboardWidgetDetailsParams, principal *models.Principal) middleware.Responder { + return fn(params, principal) +} + +// DashboardWidgetDetailsHandler interface for that can handle valid dashboard widget details params +type DashboardWidgetDetailsHandler interface { + Handle(DashboardWidgetDetailsParams, *models.Principal) middleware.Responder +} + +// NewDashboardWidgetDetails creates a new http.Handler for the dashboard widget details operation +func NewDashboardWidgetDetails(ctx *middleware.Context, handler DashboardWidgetDetailsHandler) *DashboardWidgetDetails { + return &DashboardWidgetDetails{Context: ctx, Handler: handler} +} + +/*DashboardWidgetDetails swagger:route GET /admin/info/widgets/{widgetId} AdminAPI dashboardWidgetDetails + +Returns information about the deployment + +*/ +type DashboardWidgetDetails struct { + Context *middleware.Context + Handler DashboardWidgetDetailsHandler +} + +func (o *DashboardWidgetDetails) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + r = rCtx + } + var Params = NewDashboardWidgetDetailsParams() + + uprinc, aCtx, err := o.Context.Authorize(r, route) + if err != nil { + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + if aCtx != nil { + r = aCtx + } + var principal *models.Principal + if uprinc != nil { + principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise + } + + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params, principal) // actually handle the request + + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/restapi/operations/admin_api/dashboard_widget_details_parameters.go b/restapi/operations/admin_api/dashboard_widget_details_parameters.go new file mode 100644 index 000000000..08d8f101c --- /dev/null +++ b/restapi/operations/admin_api/dashboard_widget_details_parameters.go @@ -0,0 +1,190 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// NewDashboardWidgetDetailsParams creates a new DashboardWidgetDetailsParams object +// no default values defined in spec. +func NewDashboardWidgetDetailsParams() DashboardWidgetDetailsParams { + + return DashboardWidgetDetailsParams{} +} + +// DashboardWidgetDetailsParams contains all the bound params for the dashboard widget details operation +// typically these are obtained from a http.Request +// +// swagger:parameters DashboardWidgetDetails +type DashboardWidgetDetailsParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + In: query + */ + End *int64 + /* + In: query + */ + Start *int64 + /* + In: query + */ + Step *int32 + /* + Required: true + In: path + */ + WidgetID int32 +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewDashboardWidgetDetailsParams() beforehand. +func (o *DashboardWidgetDetailsParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + qs := runtime.Values(r.URL.Query()) + + qEnd, qhkEnd, _ := qs.GetOK("end") + if err := o.bindEnd(qEnd, qhkEnd, route.Formats); err != nil { + res = append(res, err) + } + + qStart, qhkStart, _ := qs.GetOK("start") + if err := o.bindStart(qStart, qhkStart, route.Formats); err != nil { + res = append(res, err) + } + + qStep, qhkStep, _ := qs.GetOK("step") + if err := o.bindStep(qStep, qhkStep, route.Formats); err != nil { + res = append(res, err) + } + + rWidgetID, rhkWidgetID, _ := route.Params.GetOK("widgetId") + if err := o.bindWidgetID(rWidgetID, rhkWidgetID, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindEnd binds and validates parameter End from query. +func (o *DashboardWidgetDetailsParams) bindEnd(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + if raw == "" { // empty values pass all other validations + return nil + } + + value, err := swag.ConvertInt64(raw) + if err != nil { + return errors.InvalidType("end", "query", "int64", raw) + } + o.End = &value + + return nil +} + +// bindStart binds and validates parameter Start from query. +func (o *DashboardWidgetDetailsParams) bindStart(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + if raw == "" { // empty values pass all other validations + return nil + } + + value, err := swag.ConvertInt64(raw) + if err != nil { + return errors.InvalidType("start", "query", "int64", raw) + } + o.Start = &value + + return nil +} + +// bindStep binds and validates parameter Step from query. +func (o *DashboardWidgetDetailsParams) bindStep(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + if raw == "" { // empty values pass all other validations + return nil + } + + value, err := swag.ConvertInt32(raw) + if err != nil { + return errors.InvalidType("step", "query", "int32", raw) + } + o.Step = &value + + return nil +} + +// bindWidgetID binds and validates parameter WidgetID from path. +func (o *DashboardWidgetDetailsParams) bindWidgetID(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + + value, err := swag.ConvertInt32(raw) + if err != nil { + return errors.InvalidType("widgetId", "path", "int32", raw) + } + o.WidgetID = value + + return nil +} diff --git a/restapi/operations/admin_api/dashboard_widget_details_responses.go b/restapi/operations/admin_api/dashboard_widget_details_responses.go new file mode 100644 index 000000000..f20f0f962 --- /dev/null +++ b/restapi/operations/admin_api/dashboard_widget_details_responses.go @@ -0,0 +1,133 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/minio/console/models" +) + +// DashboardWidgetDetailsOKCode is the HTTP code returned for type DashboardWidgetDetailsOK +const DashboardWidgetDetailsOKCode int = 200 + +/*DashboardWidgetDetailsOK A successful response. + +swagger:response dashboardWidgetDetailsOK +*/ +type DashboardWidgetDetailsOK struct { + + /* + In: Body + */ + Payload *models.WidgetDetails `json:"body,omitempty"` +} + +// NewDashboardWidgetDetailsOK creates DashboardWidgetDetailsOK with default headers values +func NewDashboardWidgetDetailsOK() *DashboardWidgetDetailsOK { + + return &DashboardWidgetDetailsOK{} +} + +// WithPayload adds the payload to the dashboard widget details o k response +func (o *DashboardWidgetDetailsOK) WithPayload(payload *models.WidgetDetails) *DashboardWidgetDetailsOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the dashboard widget details o k response +func (o *DashboardWidgetDetailsOK) SetPayload(payload *models.WidgetDetails) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DashboardWidgetDetailsOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +/*DashboardWidgetDetailsDefault Generic error response. + +swagger:response dashboardWidgetDetailsDefault +*/ +type DashboardWidgetDetailsDefault struct { + _statusCode int + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewDashboardWidgetDetailsDefault creates DashboardWidgetDetailsDefault with default headers values +func NewDashboardWidgetDetailsDefault(code int) *DashboardWidgetDetailsDefault { + if code <= 0 { + code = 500 + } + + return &DashboardWidgetDetailsDefault{ + _statusCode: code, + } +} + +// WithStatusCode adds the status to the dashboard widget details default response +func (o *DashboardWidgetDetailsDefault) WithStatusCode(code int) *DashboardWidgetDetailsDefault { + o._statusCode = code + return o +} + +// SetStatusCode sets the status to the dashboard widget details default response +func (o *DashboardWidgetDetailsDefault) SetStatusCode(code int) { + o._statusCode = code +} + +// WithPayload adds the payload to the dashboard widget details default response +func (o *DashboardWidgetDetailsDefault) WithPayload(payload *models.Error) *DashboardWidgetDetailsDefault { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the dashboard widget details default response +func (o *DashboardWidgetDetailsDefault) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DashboardWidgetDetailsDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(o._statusCode) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/restapi/operations/admin_api/dashboard_widget_details_urlbuilder.go b/restapi/operations/admin_api/dashboard_widget_details_urlbuilder.go new file mode 100644 index 000000000..9ef236e35 --- /dev/null +++ b/restapi/operations/admin_api/dashboard_widget_details_urlbuilder.go @@ -0,0 +1,150 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package admin_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" + + "github.com/go-openapi/swag" +) + +// DashboardWidgetDetailsURL generates an URL for the dashboard widget details operation +type DashboardWidgetDetailsURL struct { + WidgetID int32 + + End *int64 + Start *int64 + Step *int32 + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *DashboardWidgetDetailsURL) WithBasePath(bp string) *DashboardWidgetDetailsURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *DashboardWidgetDetailsURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *DashboardWidgetDetailsURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/admin/info/widgets/{widgetId}" + + widgetID := swag.FormatInt32(o.WidgetID) + if widgetID != "" { + _path = strings.Replace(_path, "{widgetId}", widgetID, -1) + } else { + return nil, errors.New("widgetId is required on DashboardWidgetDetailsURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/api/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + qs := make(url.Values) + + var endQ string + if o.End != nil { + endQ = swag.FormatInt64(*o.End) + } + if endQ != "" { + qs.Set("end", endQ) + } + + var startQ string + if o.Start != nil { + startQ = swag.FormatInt64(*o.Start) + } + if startQ != "" { + qs.Set("start", startQ) + } + + var stepQ string + if o.Step != nil { + stepQ = swag.FormatInt32(*o.Step) + } + if stepQ != "" { + qs.Set("step", stepQ) + } + + _result.RawQuery = qs.Encode() + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *DashboardWidgetDetailsURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *DashboardWidgetDetailsURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *DashboardWidgetDetailsURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on DashboardWidgetDetailsURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on DashboardWidgetDetailsURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *DashboardWidgetDetailsURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/restapi/operations/console_api.go b/restapi/operations/console_api.go index 7240a0067..89d34d033 100644 --- a/restapi/operations/console_api.go +++ b/restapi/operations/console_api.go @@ -115,6 +115,9 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI { AdminAPICreateTenantHandler: admin_api.CreateTenantHandlerFunc(func(params admin_api.CreateTenantParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation admin_api.CreateTenant has not yet been implemented") }), + AdminAPIDashboardWidgetDetailsHandler: admin_api.DashboardWidgetDetailsHandlerFunc(func(params admin_api.DashboardWidgetDetailsParams, principal *models.Principal) middleware.Responder { + return middleware.NotImplemented("operation admin_api.DashboardWidgetDetails has not yet been implemented") + }), UserAPIDeleteBucketHandler: user_api.DeleteBucketHandlerFunc(func(params user_api.DeleteBucketParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation user_api.DeleteBucket has not yet been implemented") }), @@ -480,6 +483,8 @@ type ConsoleAPI struct { UserAPICreateServiceAccountHandler user_api.CreateServiceAccountHandler // AdminAPICreateTenantHandler sets the operation handler for the create tenant operation AdminAPICreateTenantHandler admin_api.CreateTenantHandler + // AdminAPIDashboardWidgetDetailsHandler sets the operation handler for the dashboard widget details operation + AdminAPIDashboardWidgetDetailsHandler admin_api.DashboardWidgetDetailsHandler // UserAPIDeleteBucketHandler sets the operation handler for the delete bucket operation UserAPIDeleteBucketHandler user_api.DeleteBucketHandler // UserAPIDeleteBucketEventHandler sets the operation handler for the delete bucket event operation @@ -793,6 +798,9 @@ func (o *ConsoleAPI) Validate() error { if o.AdminAPICreateTenantHandler == nil { unregistered = append(unregistered, "admin_api.CreateTenantHandler") } + if o.AdminAPIDashboardWidgetDetailsHandler == nil { + unregistered = append(unregistered, "admin_api.DashboardWidgetDetailsHandler") + } if o.UserAPIDeleteBucketHandler == nil { unregistered = append(unregistered, "user_api.DeleteBucketHandler") } @@ -1242,6 +1250,10 @@ func (o *ConsoleAPI) initHandlerCache() { o.handlers["POST"] = make(map[string]http.Handler) } o.handlers["POST"]["/tenants"] = admin_api.NewCreateTenant(o.context, o.AdminAPICreateTenantHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } + o.handlers["GET"]["/admin/info/widgets/{widgetId}"] = admin_api.NewDashboardWidgetDetails(o.context, o.AdminAPIDashboardWidgetDetailsHandler) if o.handlers["DELETE"] == nil { o.handlers["DELETE"] = make(map[string]http.Handler) } diff --git a/swagger.yml b/swagger.yml index eb89b538d..b8e7ae624 100644 --- a/swagger.yml +++ b/swagger.yml @@ -1773,7 +1773,28 @@ paths: get: summary: Returns information about the deployment operationId: AdminInfo + responses: + 200: + description: A successful response. + schema: + $ref: "#/definitions/adminInfoResponse" + default: + description: Generic error response. + schema: + $ref: "#/definitions/error" + tags: + - AdminAPI + + /admin/info/widgets/{widgetId}: + get: + summary: Returns information about the deployment + operationId: DashboardWidgetDetails parameters: + - name: widgetId + in: path + type: integer + format: int32 + required: true - name: start in: query type: integer @@ -1788,7 +1809,7 @@ paths: 200: description: A successful response. schema: - $ref: "#/definitions/adminInfoResponse" + $ref: "#/definitions/widgetDetails" default: description: Generic error response. schema: @@ -3290,6 +3311,33 @@ definitions: type: string type: type: string + id: + type: integer + format: int32 + options: + type: object + properties: + reduceOptions: + type: object + properties: + calcs: + type: array + items: + type: string + targets: + type: array + items: + $ref: "#/definitions/resultTarget" + widgetDetails: + type: object + properties: + title: + type: string + type: + type: string + id: + type: integer + format: int32 options: type: object properties: