From e8d5bfe5476ad34062d0595c6e9706e466b02b60 Mon Sep 17 00:00:00 2001 From: ricoberger Date: Sat, 1 Jan 2022 15:12:09 +0100 Subject: [PATCH] [azure] Improve cost management integration Improve the cost management integration in the Azure plugin. The pie chart for the actual costs uses the same style as the other charts in kobs. We are now using the chart theme, color scheme and tooltip from the core package, so that the chart fits the style of the other charts in kobs and the Azure plugin. Instead of selecting the time from a dropdown in the cost management page, it is now possible to select a time range via the Toolbar component from the core package, like it is done in other plugins. It is now possible to show the actual costs in a panel on a dashboard. For this the type "costmanagement" with the sub type "actualcosts" can be selected in the Azure plugin options. The user can also set a scope (name of a resource group). If the user doesn't set this value it will default to "All". --- CHANGELOG.md | 1 + docs/plugins/azure.md | 10 +++- plugins/azure/costmanagement.go | 28 +++++---- plugins/azure/costmanagement_test.go | 26 ++++++--- .../instance/costmanagement/costmanagement.go | 57 +------------------ .../costmanagement/costmanagement_mock.go | 14 ++--- .../pkg/instance/costmanagement/helpers.go | 57 +++++++++++++++++++ .../instance/costmanagement/helpers_test.go | 28 +++++++++ .../components/costmanagement/ActualCosts.tsx | 36 ++++++++---- .../costmanagement/CostManagementToolbar.tsx | 47 +++++++-------- .../CostManagementToolbarItemScope.tsx | 2 +- .../costmanagement/CostPieChart.tsx | 39 +++++++++---- .../src/components/costmanagement/Page.tsx | 43 ++++++++++---- .../src/components/costmanagement/helpers.ts | 13 +++++ .../components/costmanagement/interfaces.ts | 9 +++ plugins/azure/src/components/panel/Panel.tsx | 16 ++++++ plugins/azure/src/utils/interfaces.ts | 4 ++ 17 files changed, 292 insertions(+), 138 deletions(-) create mode 100644 plugins/azure/pkg/instance/costmanagement/helpers.go create mode 100644 plugins/azure/pkg/instance/costmanagement/helpers_test.go create mode 100644 plugins/azure/src/components/costmanagement/helpers.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ebcb9d842..54adf3984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan - [#263](https://github.com/kobsio/kobs/pull/263): [core] :warning: _Breaking change:_ :warning: Refactor `cluster` and `clusters` package. - [#265](https://github.com/kobsio/kobs/pull/265): [applications] Improve tags support by allow users to filter applications by tags and showing tags on application page. - [#269](https://github.com/kobsio/kobs/pull/269): [applications] :warning: _Breaking change:_ :warning: Improve topology graph, by allowing custom styles for applications. +- [#275](https://github.com/kobsio/kobs/pull/275): [azure] Improve cost management integration by adjusting the chart style and allowing the usage in dashboard panels. ## [v0.7.0](https://github.com/kobsio/kobs/releases/tag/v0.7.0) (2021-11-19) diff --git a/docs/plugins/azure.md b/docs/plugins/azure.md index 431286188..3e78027e9 100644 --- a/docs/plugins/azure.md +++ b/docs/plugins/azure.md @@ -46,8 +46,9 @@ The following options can be used for a panel with the Azure plugin: | Field | Type | Description | Required | | ----- | ---- | ----------- | -------- | -| type | string | The service type which should be used for the panel Currently `containerinstances`, `kubernetesservices` and `virtualmachinescalesets` are supported values. | Yes | +| type | string | The service type which should be used for the panel Currently `containerinstances`, `costmanagement`, `kubernetesservices` and `virtualmachinescalesets` are supported values. | Yes | | containerinstances | [Container Instances](#container-instances) | The configuration for the panel if the type is `containerinstances`. | No | +| costmanagement | [Cost Management](#cost-management) | The configuration for the panel if the type is `costmanagement`. | No | | kubernetesservices | [Kubernetes Services](#kubernetes-services) | The configuration for the panel if the type is `kubernetesservices`. | No | | virtualmachinescalesets | [Virtual Machine Scale Sets](#virtual-machine-scale-sets) | The configuration for the panel if the type is `virtualmachinescalesets`. | No | @@ -63,6 +64,13 @@ The following options can be used for a panel with the Azure plugin: | metricNames | string | The name of the metric for which the data should be displayed. Supported values are `CPUUsage`, `MemoryUsage`, `NetworkBytesReceivedPerSecond` and `NetworkBytesTransmittedPerSecond`. This is only required if the type is `metrics`. | No | | aggregationType | string | The aggregation type for the metric. Supported values are `Average`, `Minimum`, `Maximum`, `Total` and `Count`. This is only required if the type is `metrics`. | No | +### Cost Management + +| Field | Type | Description | Required | +| ----- | ---- | ----------- | -------- | +| type | string | The type of the panel for which the Cost Management data should be displayed. Currently only `actualcosts` is supported. | Yes | +| scope | string | The scope for the costs data. This could be the name of a resource group or `All`. The default value is `All`. | No | + ### Kubernetes Services | Field | Type | Description | Required | diff --git a/plugins/azure/costmanagement.go b/plugins/azure/costmanagement.go index ac928ebf2..bf9c7c9e5 100644 --- a/plugins/azure/costmanagement.go +++ b/plugins/azure/costmanagement.go @@ -14,17 +14,11 @@ import ( func (router *Router) getActualCosts(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") - timeframe := r.URL.Query().Get("timeframe") scope := r.URL.Query().Get("scope") + timeStart := r.URL.Query().Get("timeStart") + timeEnd := r.URL.Query().Get("timeEnd") - log.Debug(r.Context(), "Get actual costs parameters.", zap.String("name", name), zap.String("timeframe", timeframe), zap.String("scope", scope)) - - timeframeParsed, err := strconv.Atoi(timeframe) - if err != nil { - log.Error(r.Context(), "Invalid timeframe parameter.", zap.Error(err)) - errresponse.Render(w, r, nil, http.StatusBadRequest, "Invalid timeframe parameter") - return - } + log.Debug(r.Context(), "Get actual costs parameters.", zap.String("name", name), zap.String("scope", scope), zap.String("timeStart", timeStart), zap.String("timeEnd", timeEnd)) i := router.getInstance(name) if i == nil { @@ -33,7 +27,21 @@ func (router *Router) getActualCosts(w http.ResponseWriter, r *http.Request) { return } - costUsage, err := i.CostManagementClient().GetActualCost(r.Context(), timeframeParsed, scope) + parsedTimeStart, err := strconv.ParseInt(timeStart, 10, 64) + if err != nil { + log.Error(r.Context(), "Could not parse start time.", zap.Error(err)) + errresponse.Render(w, r, err, http.StatusBadRequest, "Could not parse start time") + return + } + + parsedTimeEnd, err := strconv.ParseInt(timeEnd, 10, 64) + if err != nil { + log.Error(r.Context(), "Could not parse end time.", zap.Error(err)) + errresponse.Render(w, r, err, http.StatusBadRequest, "Could not parse end time") + return + } + + costUsage, err := i.CostManagementClient().GetActualCost(r.Context(), scope, parsedTimeStart, parsedTimeEnd) if err != nil { log.Error(r.Context(), "Could not query cost usage.", zap.Error(err)) errresponse.Render(w, r, err, http.StatusInternalServerError, "Could not query cost usage") diff --git a/plugins/azure/costmanagement_test.go b/plugins/azure/costmanagement_test.go index 5ae61e5df..6b7e6f29c 100644 --- a/plugins/azure/costmanagement_test.go +++ b/plugins/azure/costmanagement_test.go @@ -24,10 +24,22 @@ func TestGetActualCosts(t *testing.T) { do func(router Router, w *httptest.ResponseRecorder, req *http.Request) }{ { - name: "invalid timeframe parameter", + name: "invalid instance name", + url: "/invalidname/costmanagement/actualcosts", + expectedStatusCode: http.StatusBadRequest, + expectedBody: "{\"error\":\"Could not find instance name\"}\n", + prepare: func(mockClient *costmanagement.MockClient, mockInstance *instance.MockInstance) { + mockInstance.On("GetName").Return("azure") + }, + do: func(router Router, w *httptest.ResponseRecorder, req *http.Request) { + router.getActualCosts(w, req) + }, + }, + { + name: "invalid start time", url: "/azure/costmanagement/actualcosts", expectedStatusCode: http.StatusBadRequest, - expectedBody: "{\"error\":\"Invalid timeframe parameter\"}\n", + expectedBody: "{\"error\":\"Could not parse start time: strconv.ParseInt: parsing \\\"\\\": invalid syntax\"}\n", prepare: func(mockClient *costmanagement.MockClient, mockInstance *instance.MockInstance) { mockInstance.On("GetName").Return("azure") }, @@ -36,10 +48,10 @@ func TestGetActualCosts(t *testing.T) { }, }, { - name: "invalid instance name", - url: "/invalidname/costmanagement/actualcosts?timeframe=3600", + name: "invalid end time", + url: "/azure/costmanagement/actualcosts?timeStart=1", expectedStatusCode: http.StatusBadRequest, - expectedBody: "{\"error\":\"Could not find instance name\"}\n", + expectedBody: "{\"error\":\"Could not parse end time: strconv.ParseInt: parsing \\\"\\\": invalid syntax\"}\n", prepare: func(mockClient *costmanagement.MockClient, mockInstance *instance.MockInstance) { mockInstance.On("GetName").Return("azure") }, @@ -51,11 +63,11 @@ func TestGetActualCosts(t *testing.T) { // panic: interface conversion: *costmanagement.MockClient is not costmanagement.Client: missing method GetActualCost // { // name: "could not get actual costs", - // url: "/azure/costmanagement/actualcosts?timeframe=3600", + // url: "/azure/costmanagement/actualcosts?timeStart=1&timeEnd=1", // expectedStatusCode: http.StatusBadRequest, // expectedBody: "{\"error\":\"Could not find instance name\"}\n", // prepare: func(mockClient *costmanagement.MockClient, mockInstance *instance.MockInstance) { - // mockClient.On("GetActualCost", mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("could not get costs")) + // mockClient.On("GetActualCost", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("could not get costs")) // mockInstance.On("GetName").Return("azure") // mockInstance.On("CostManagementClient").Return(mockClient) diff --git a/plugins/azure/pkg/instance/costmanagement/costmanagement.go b/plugins/azure/pkg/instance/costmanagement/costmanagement.go index f9e882736..57acc6a9f 100644 --- a/plugins/azure/pkg/instance/costmanagement/costmanagement.go +++ b/plugins/azure/pkg/instance/costmanagement/costmanagement.go @@ -3,17 +3,14 @@ package costmanagement import ( "context" "fmt" - "time" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/services/costmanagement/mgmt/2019-11-01/costmanagement" "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/date" ) // Client is the interface for a client to interact with the Azure cost management api. type Client interface { - GetActualCost(ctx context.Context, timeframe int, scope string) (costmanagement.QueryResult, error) + GetActualCost(ctx context.Context, scope string, timeStart, timeEnd int64) (costmanagement.QueryResult, error) } type client struct { @@ -22,7 +19,7 @@ type client struct { } // GetActualCost query the actual costs for the configured subscription and given timeframe grouped by resourceGroup -func (c *client) GetActualCost(ctx context.Context, timeframe int, scope string) (costmanagement.QueryResult, error) { +func (c *client) GetActualCost(ctx context.Context, scope string, timeStart, timeEnd int64) (costmanagement.QueryResult, error) { var queryScope string var subscriptionScope bool @@ -33,55 +30,7 @@ func (c *client) GetActualCost(ctx context.Context, timeframe int, scope string) queryScope = fmt.Sprintf("subscriptions/%s/resourceGroups/%s", c.subscriptionID, scope) } - return c.queryClient.Usage(ctx, queryScope, buildQueryParams(timeframe, subscriptionScope)) -} - -func buildQueryParams(timeframe int, subscriptionScope bool) costmanagement.QueryDefinition { - agg := make(map[string]*costmanagement.QueryAggregation) - tc := costmanagement.QueryAggregation{ - Name: to.StringPtr("Cost"), - Function: costmanagement.FunctionTypeSum, - } - agg["totalCost"] = &tc - - var grouping []costmanagement.QueryGrouping - if subscriptionScope { - grouping = []costmanagement.QueryGrouping{ - { - Type: costmanagement.QueryColumnTypeDimension, - Name: to.StringPtr("resourceGroup"), - }, - } - } else { - grouping = []costmanagement.QueryGrouping{ - { - Type: costmanagement.QueryColumnTypeDimension, - Name: to.StringPtr("ServiceName"), - }, - } - } - - ds := costmanagement.QueryDataset{ - Granularity: "None", - Configuration: nil, - Aggregation: agg, - Grouping: &grouping, - Filter: nil, - } - - now := date.Time{Time: time.Now()} - from := date.Time{Time: now.AddDate(0, 0, timeframe*-1)} - tp := costmanagement.QueryTimePeriod{ - From: &from, - To: &now, - } - - return costmanagement.QueryDefinition{ - Type: costmanagement.ExportTypeActualCost, - Timeframe: costmanagement.TimeframeTypeCustom, - TimePeriod: &tp, - Dataset: &ds, - } + return c.queryClient.Usage(ctx, queryScope, buildQueryParams(subscriptionScope, timeStart, timeEnd)) } // New returns a new client to interact with the cost management API. diff --git a/plugins/azure/pkg/instance/costmanagement/costmanagement_mock.go b/plugins/azure/pkg/instance/costmanagement/costmanagement_mock.go index 9536aa79e..bdd329e48 100644 --- a/plugins/azure/pkg/instance/costmanagement/costmanagement_mock.go +++ b/plugins/azure/pkg/instance/costmanagement/costmanagement_mock.go @@ -14,20 +14,20 @@ type MockClient struct { mock.Mock } -// GetActualCost provides a mock function with given fields: ctx, timeframe, scope -func (_m *MockClient) GetActualCost(ctx context.Context, timeframe int, scope string) (costmanagement.QueryResult, error) { - ret := _m.Called(ctx, timeframe, scope) +// GetActualCost provides a mock function with given fields: ctx, scope, timeStart, timeEnd +func (_m *MockClient) GetActualCost(ctx context.Context, scope string, timeStart int64, timeEnd int64) (costmanagement.QueryResult, error) { + ret := _m.Called(ctx, scope, timeStart, timeEnd) var r0 costmanagement.QueryResult - if rf, ok := ret.Get(0).(func(context.Context, int, string) costmanagement.QueryResult); ok { - r0 = rf(ctx, timeframe, scope) + if rf, ok := ret.Get(0).(func(context.Context, string, int64, int64) costmanagement.QueryResult); ok { + r0 = rf(ctx, scope, timeStart, timeEnd) } else { r0 = ret.Get(0).(costmanagement.QueryResult) } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, int, string) error); ok { - r1 = rf(ctx, timeframe, scope) + if rf, ok := ret.Get(1).(func(context.Context, string, int64, int64) error); ok { + r1 = rf(ctx, scope, timeStart, timeEnd) } else { r1 = ret.Error(1) } diff --git a/plugins/azure/pkg/instance/costmanagement/helpers.go b/plugins/azure/pkg/instance/costmanagement/helpers.go new file mode 100644 index 000000000..3a4acf28d --- /dev/null +++ b/plugins/azure/pkg/instance/costmanagement/helpers.go @@ -0,0 +1,57 @@ +package costmanagement + +import ( + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/services/costmanagement/mgmt/2019-11-01/costmanagement" + "github.com/Azure/go-autorest/autorest/date" +) + +func buildQueryParams(subscriptionScope bool, timeStart, timeEnd int64) costmanagement.QueryDefinition { + agg := make(map[string]*costmanagement.QueryAggregation) + tc := costmanagement.QueryAggregation{ + Name: to.StringPtr("Cost"), + Function: costmanagement.FunctionTypeSum, + } + agg["totalCost"] = &tc + + var grouping []costmanagement.QueryGrouping + if subscriptionScope { + grouping = []costmanagement.QueryGrouping{ + { + Type: costmanagement.QueryColumnTypeDimension, + Name: to.StringPtr("resourceGroup"), + }, + } + } else { + grouping = []costmanagement.QueryGrouping{ + { + Type: costmanagement.QueryColumnTypeDimension, + Name: to.StringPtr("ServiceName"), + }, + } + } + + ds := costmanagement.QueryDataset{ + Granularity: "None", + Configuration: nil, + Aggregation: agg, + Grouping: &grouping, + Filter: nil, + } + + now := date.Time{Time: time.Unix(timeEnd, 0)} + from := date.Time{Time: time.Unix(timeStart, 0)} + tp := costmanagement.QueryTimePeriod{ + From: &from, + To: &now, + } + + return costmanagement.QueryDefinition{ + Type: costmanagement.ExportTypeActualCost, + Timeframe: costmanagement.TimeframeTypeCustom, + TimePeriod: &tp, + Dataset: &ds, + } +} diff --git a/plugins/azure/pkg/instance/costmanagement/helpers_test.go b/plugins/azure/pkg/instance/costmanagement/helpers_test.go new file mode 100644 index 000000000..3a7a0e3a6 --- /dev/null +++ b/plugins/azure/pkg/instance/costmanagement/helpers_test.go @@ -0,0 +1,28 @@ +package costmanagement + +import ( + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/services/costmanagement/mgmt/2019-11-01/costmanagement" + "github.com/Azure/go-autorest/autorest/date" + "github.com/stretchr/testify/require" +) + +func TestBuildQueryParams(t *testing.T) { + t.Run("subscription scope", func(t *testing.T) { + actualQueryParams := buildQueryParams(true, 0, 1) + + require.Equal(t, costmanagement.ExportTypeActualCost, actualQueryParams.Type) + require.Equal(t, costmanagement.TimeframeTypeCustom, actualQueryParams.Timeframe) + require.Equal(t, costmanagement.QueryTimePeriod{From: &date.Time{Time: time.Unix(0, 0)}, To: &date.Time{Time: time.Unix(1, 0)}}, *actualQueryParams.TimePeriod) + }) + + t.Run("not subscription scope", func(t *testing.T) { + actualQueryParams := buildQueryParams(false, 0, 1) + + require.Equal(t, costmanagement.ExportTypeActualCost, actualQueryParams.Type) + require.Equal(t, costmanagement.TimeframeTypeCustom, actualQueryParams.Timeframe) + require.Equal(t, costmanagement.QueryTimePeriod{From: &date.Time{Time: time.Unix(0, 0)}, To: &date.Time{Time: time.Unix(1, 0)}}, *actualQueryParams.TimePeriod) + }) +} diff --git a/plugins/azure/src/components/costmanagement/ActualCosts.tsx b/plugins/azure/src/components/costmanagement/ActualCosts.tsx index fd6e70e44..5fc57a9cf 100644 --- a/plugins/azure/src/components/costmanagement/ActualCosts.tsx +++ b/plugins/azure/src/components/costmanagement/ActualCosts.tsx @@ -3,24 +3,22 @@ import { QueryObserverResult, useQuery } from 'react-query'; import React from 'react'; import { CostPieChart } from './CostPieChart'; +import { IPluginTimes } from '@kobsio/plugin-core'; import { IQueryResult } from './interfaces'; interface IActualCostsProps { name: string; - timeframe: number; scope: string; + times: IPluginTimes; } -const ActualCosts: React.FunctionComponent = ({ name, timeframe, scope }: IActualCostsProps) => { +const ActualCosts: React.FunctionComponent = ({ name, scope, times }: IActualCostsProps) => { const { isError, isLoading, error, data, refetch } = useQuery( - ['azure/costmanagement/actualcost', name, timeframe, scope], + ['azure/costmanagement/actualcost', name, scope, times], async () => { try { - const timeframeParam = `timeframe=${timeframe}`; - const scopeParam = `scope=${scope}`; - const response = await fetch( - `/api/plugins/azure/${name}/costmanagement/actualcosts?${timeframeParam}&${scopeParam}`, + `/api/plugins/azure/${name}/costmanagement/actualcosts?scope=${scope}&timeStart=${times.timeStart}&timeEnd=${times.timeEnd}`, { method: 'get', }, @@ -55,6 +53,7 @@ const ActualCosts: React.FunctionComponent = ({ name, timefra return ( @@ -69,13 +68,28 @@ const ActualCosts: React.FunctionComponent = ({ name, timefra ); } - if (!data) { - return null; + if (!data || !data.properties || data.properties.rows.length === 0) { + return ( + + > => refetch()}> + Retry + + + } + > +

For the selected scope or time range was no cost data found.

+
+ ); } return ( -
- +
+ ;
); }; diff --git a/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx b/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx index df080bc63..2f6fa0dc8 100644 --- a/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx +++ b/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx @@ -1,39 +1,36 @@ -import { Toolbar, ToolbarContent, ToolbarItem, ToolbarToggleGroup } from '@patternfly/react-core'; -import { FilterIcon } from '@patternfly/react-icons'; -import React from 'react'; +import React, { useState } from 'react'; +import { ToolbarItem } from '@patternfly/react-core'; +import { IOptionsAdditionalFields, IPluginTimes, Toolbar } from '@kobsio/plugin-core'; import CostManagementToolbarItemScope from './CostManagementToolbarItemScope'; -import CostManagementToolbarItemTimeframe from './CostManagementToolbarItemTimeframe'; +import { IOptions } from './interfaces'; export interface ICostManagementToolbarProps { - timeframe: number; - setTimeframe: (timeframe: number) => void; - scope: string; - setScope: (scope: string) => void; resourceGroups: string[]; + options: IOptions; + setOptions: (data: IOptions) => void; } const CostManagementToolbar: React.FunctionComponent = ({ - timeframe, - setTimeframe, - scope, - setScope, resourceGroups, + options, + setOptions, }: ICostManagementToolbarProps) => { + const [scope, setScope] = useState(options.scope); + + const changeOptions = (times: IPluginTimes, additionalFields: IOptionsAdditionalFields[] | undefined): void => { + setOptions({ + scope: scope, + times: times, + }); + }; + return ( - - - } breakpoint="lg"> - Scope - - - - Timeframe - - - - - + + Scope + + + ); }; diff --git a/plugins/azure/src/components/costmanagement/CostManagementToolbarItemScope.tsx b/plugins/azure/src/components/costmanagement/CostManagementToolbarItemScope.tsx index 3b0e2e3e0..78c63b4de 100644 --- a/plugins/azure/src/components/costmanagement/CostManagementToolbarItemScope.tsx +++ b/plugins/azure/src/components/costmanagement/CostManagementToolbarItemScope.tsx @@ -16,7 +16,7 @@ const CostManagementToolbarItemScope: React.FunctionComponent(false); const options = resourceGroups; if (options.indexOf('All') === -1) { - options.push('All'); + options.unshift('All'); } return ( diff --git a/plugins/azure/src/components/costmanagement/CostPieChart.tsx b/plugins/azure/src/components/costmanagement/CostPieChart.tsx index 09cdd7681..039a116d8 100644 --- a/plugins/azure/src/components/costmanagement/CostPieChart.tsx +++ b/plugins/azure/src/components/costmanagement/CostPieChart.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { ResponsivePie } from '@nivo/pie'; +import { CHART_THEME, COLOR_SCALE, ChartTooltip } from '@kobsio/plugin-core'; import { IQueryResult } from './interfaces'; import { convertQueryResult } from '../../utils/helpers'; @@ -11,23 +12,19 @@ interface ICostPieChartProps { export const CostPieChart: React.FunctionComponent = ({ data }: ICostPieChartProps) => { return ( = ({ data translateY: 0, }, ]} + margin={{ bottom: 80, left: 80, right: 80, top: 40 }} + motionConfig="gentle" + padAngle={0.7} + theme={CHART_THEME} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + tooltip={(tooltip) => { + let currency = ''; + const row = data.properties.rows.filter((row) => row[1] === tooltip.datum.label.toString()); + + if (row.length === 1) { + currency = row[0][2]; + } + + return ( + + ); + }} + valueFormat=" >-.2f" /> ); }; diff --git a/plugins/azure/src/components/costmanagement/Page.tsx b/plugins/azure/src/components/costmanagement/Page.tsx index 38c58580f..dde4a84ae 100644 --- a/plugins/azure/src/components/costmanagement/Page.tsx +++ b/plugins/azure/src/components/costmanagement/Page.tsx @@ -1,9 +1,12 @@ -import { PageSection, PageSectionVariants } from '@patternfly/react-core'; -import React, { useState } from 'react'; +import { Card, CardBody, PageSection, PageSectionVariants } from '@patternfly/react-core'; +import React, { useEffect, useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; import ActualCosts from './ActualCosts'; import CostManagementToolbar from './CostManagementToolbar'; +import { IOptions } from './interfaces'; import { Title } from '@kobsio/plugin-core'; +import { getInitialOptions } from './helpers'; import { services } from '../../utils/services'; const service = 'costmanagement'; @@ -19,25 +22,41 @@ const CostManagementPage: React.FunctionComponent = ({ displayName, resourceGroups, }: ICostManagementPageProps) => { - const [timeframe, setTimeframe] = useState(7); - const [scope, setScope] = useState('All'); + const history = useHistory(); + const location = useLocation(); + const [options, setOptions] = useState(); + + const changeOptions = (opts: IOptions): void => { + history.push({ + pathname: location.pathname, + search: `?scope=${opts.scope}&time=${opts.times.time}&timeEnd=${opts.times.timeEnd}&timeStart=${opts.times.timeStart}`, + }); + }; + + useEffect(() => { + setOptions((prevOptions) => getInitialOptions(location.search, !prevOptions)); + }, [location.search]); + + if (!options) { + return null; + } return ( <p>{services[service].description}</p> - <CostManagementToolbar - timeframe={timeframe} - setTimeframe={setTimeframe} - scope={scope} - setScope={setScope} - resourceGroups={resourceGroups} - /> + <CostManagementToolbar resourceGroups={resourceGroups} options={options} setOptions={changeOptions} /> </PageSection> <PageSection style={{ minHeight: '100%' }} variant={PageSectionVariants.default}> - <ActualCosts name={name} timeframe={timeframe} scope={scope} /> + <Card isCompact={true}> + <CardBody> + <div style={{ height: '500px' }}> + <ActualCosts name={name} scope={options.scope} times={options.times} /> + </div> + </CardBody> + </Card> </PageSection> </React.Fragment> ); diff --git a/plugins/azure/src/components/costmanagement/helpers.ts b/plugins/azure/src/components/costmanagement/helpers.ts new file mode 100644 index 000000000..9b3cc924a --- /dev/null +++ b/plugins/azure/src/components/costmanagement/helpers.ts @@ -0,0 +1,13 @@ +import { IOptions } from './interfaces'; +import { getTimeParams } from '@kobsio/plugin-core'; + +// getInitialOptions returns the initial options for the applications page from the url. +export const getInitialOptions = (search: string, isInitial: boolean): IOptions => { + const params = new URLSearchParams(search); + const scope = params.get('scope'); + + return { + scope: scope ? scope : 'All', + times: getTimeParams(params, isInitial), + }; +}; diff --git a/plugins/azure/src/components/costmanagement/interfaces.ts b/plugins/azure/src/components/costmanagement/interfaces.ts index fcdaa7c58..f0da6f8bf 100644 --- a/plugins/azure/src/components/costmanagement/interfaces.ts +++ b/plugins/azure/src/components/costmanagement/interfaces.ts @@ -1,3 +1,12 @@ +import { IPluginTimes } from '@kobsio/plugin-core'; + +// IOptions is the interface for all options for the applications page. +export interface IOptions { + scope: string; + times: IPluginTimes; +} + +// IQueryResult is the interface for the data returned by the Azure api for the actual costs. export interface IQueryResult { properties: IQueryProperties; } diff --git a/plugins/azure/src/components/panel/Panel.tsx b/plugins/azure/src/components/panel/Panel.tsx index 240316766..0f0cd5530 100644 --- a/plugins/azure/src/components/panel/Panel.tsx +++ b/plugins/azure/src/components/panel/Panel.tsx @@ -9,6 +9,8 @@ import CIDetailsContainerGroup from '../containerinstances/DetailsContainerGroup import CIDetailsContainerGroupActions from '../containerinstances/DetailsContainerGroupActions'; import CIDetailsLogs from '../containerinstances/DetailsLogs'; +import CMActualCosts from '../costmanagement/ActualCosts'; + import KSDetailsKubernetesService from '../kubernetesservices/DetailsKubernetesService'; import KSDetailsNodePoolsWrapper from '../kubernetesservices/DetailsNodePoolsWrapper'; import KSKubernetesServices from '../kubernetesservices/KubernetesServices'; @@ -130,6 +132,20 @@ export const Panel: React.FunctionComponent<IPanelProps> = ({ ); } + if ( + options?.type && + options?.type === 'costmanagement' && + options.costmanagement && + options.costmanagement.type === 'actualcosts' && + times + ) { + return ( + <PluginCard title={title} description={description}> + <CMActualCosts name={name} scope={options.costmanagement.scope || 'All'} times={times} /> + </PluginCard> + ); + } + // Panels for kubernetes services. if ( options?.type && diff --git a/plugins/azure/src/utils/interfaces.ts b/plugins/azure/src/utils/interfaces.ts index 12c7becb6..7f36fe3f7 100644 --- a/plugins/azure/src/utils/interfaces.ts +++ b/plugins/azure/src/utils/interfaces.ts @@ -10,6 +10,10 @@ export interface IPanelOptions { metricNames?: string; aggregationType?: string; }; + costmanagement?: { + type?: string; + scope?: string; + }; kubernetesservices?: { type?: string; resourceGroups?: string[];