From 8d1ad9749df3708e1b4914abef60247f2ea1b7f3 Mon Sep 17 00:00:00 2001 From: nikhagra <165884194+nikhagra@users.noreply.github.com> Date: Tue, 23 Jul 2024 21:27:12 +0530 Subject: [PATCH] upcoming: [DI-19610] - Added CloudPulse widget to show graph for different metrices (#10676) Co-authored-by: Hana Xu Co-authored-by: Jaalah Ramos Co-authored-by: Jaalah Ramos --- ...r-10676-upcoming-features-1721049858027.md | 5 + packages/api-v4/src/cloudpulse/dashboards.ts | 8 +- packages/api-v4/src/cloudpulse/index.ts | 2 + packages/api-v4/src/cloudpulse/services.ts | 24 ++ packages/api-v4/src/cloudpulse/types.ts | 28 ++ ...r-10676-upcoming-features-1721049757333.md | 5 + .../assets/icons/entityIcons/cv_overview.svg | 5 + .../features/CloudPulse/CloudPulseTabs.tsx | 8 +- .../Dashboard/CloudPulseDashboard.tsx | 241 +++++++++++++++++ .../Dashboard/CloudPulseDashboardLanding.tsx | 37 +++ .../CloudPulse/Dashboard/DashboardLanding.tsx | 29 -- .../CloudPulse/Overview/GlobalFilters.tsx | 22 ++ .../CloudPulse/Utils/UserPreference.ts | 23 +- .../features/CloudPulse/{ => Utils}/utils.ts | 20 ++ .../CloudPulse/Widget/CloudPulseWidget.tsx | 248 ++++++++++++++++++ .../CloudPulseAggregateFunction.test.tsx | 39 +++ .../CloudPulseAggregateFunction.tsx | 57 ++++ .../CloudPulseIntervalSelect.test.tsx | 28 ++ .../components/CloudPulseIntervalSelect.tsx | 132 ++++++++++ .../Widget/components/Zoomer.test.tsx | 28 ++ .../CloudPulse/Widget/components/Zoomer.tsx | 37 +++ .../shared/CloudPulseDashboardSelect.test.tsx | 8 +- .../shared/CloudPulseDashboardSelect.tsx | 6 +- .../shared/CloudPulseRegionSelect.test.tsx | 2 +- packages/manager/src/mocks/serverHandlers.ts | 181 ++++++++++++- .../src/queries/cloudpulse/dashboards.ts | 23 +- .../src/queries/cloudpulse/services.ts | 47 ++++ 27 files changed, 1244 insertions(+), 49 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-10676-upcoming-features-1721049858027.md create mode 100644 packages/api-v4/src/cloudpulse/services.ts create mode 100644 packages/manager/.changeset/pr-10676-upcoming-features-1721049757333.md create mode 100644 packages/manager/src/assets/icons/entityIcons/cv_overview.svg create mode 100644 packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx create mode 100644 packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Dashboard/DashboardLanding.tsx rename packages/manager/src/features/CloudPulse/{ => Utils}/utils.ts (57%) create mode 100644 packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx create mode 100644 packages/manager/src/features/CloudPulse/Widget/components/CloudPulseAggregateFunction.test.tsx create mode 100644 packages/manager/src/features/CloudPulse/Widget/components/CloudPulseAggregateFunction.tsx create mode 100644 packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.test.tsx create mode 100644 packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.tsx create mode 100644 packages/manager/src/features/CloudPulse/Widget/components/Zoomer.test.tsx create mode 100644 packages/manager/src/features/CloudPulse/Widget/components/Zoomer.tsx create mode 100644 packages/manager/src/queries/cloudpulse/services.ts diff --git a/packages/api-v4/.changeset/pr-10676-upcoming-features-1721049858027.md b/packages/api-v4/.changeset/pr-10676-upcoming-features-1721049858027.md new file mode 100644 index 00000000000..f05ef6b0591 --- /dev/null +++ b/packages/api-v4/.changeset/pr-10676-upcoming-features-1721049858027.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +Added MetricDefinitions, Dimension, JWETokenPayload, JWEToken and metricDefinitions, dashboard by id and jwe token api calls ([#10676](https://github.com/linode/manager/pull/10676)) diff --git a/packages/api-v4/src/cloudpulse/dashboards.ts b/packages/api-v4/src/cloudpulse/dashboards.ts index 46755363f44..90b4d4ef010 100644 --- a/packages/api-v4/src/cloudpulse/dashboards.ts +++ b/packages/api-v4/src/cloudpulse/dashboards.ts @@ -3,9 +3,15 @@ import Request, { setMethod, setURL } from '../request'; import { Dashboard } from './types'; import { API_ROOT } from 'src/constants'; -//Returns the list of all the dashboards available +// Returns the list of all the dashboards available export const getDashboards = () => Request>( setURL(`${API_ROOT}/monitor/services/linode/dashboards`), setMethod('GET') ); + +export const getDashboardById = (dashboardId: number) => + Request( + setURL(`${API_ROOT}/monitor/dashboards/${encodeURIComponent(dashboardId)}`), + setMethod('GET') + ); diff --git a/packages/api-v4/src/cloudpulse/index.ts b/packages/api-v4/src/cloudpulse/index.ts index 25a3879e494..6b4ff9e8b55 100644 --- a/packages/api-v4/src/cloudpulse/index.ts +++ b/packages/api-v4/src/cloudpulse/index.ts @@ -1,3 +1,5 @@ export * from './types'; export * from './dashboards'; + +export * from './services'; diff --git a/packages/api-v4/src/cloudpulse/services.ts b/packages/api-v4/src/cloudpulse/services.ts new file mode 100644 index 00000000000..f8d884d572a --- /dev/null +++ b/packages/api-v4/src/cloudpulse/services.ts @@ -0,0 +1,24 @@ +import { API_ROOT } from 'src/constants'; +import Request, { setData, setMethod, setURL } from '../request'; +import { JWEToken, JWETokenPayLoad, MetricDefinitions } from './types'; +import { ResourcePage as Page } from 'src/types'; + +export const getMetricDefinitionsByServiceType = (serviceType: string) => { + return Request>( + setURL( + `${API_ROOT}/monitor/services/${encodeURIComponent( + serviceType + )}/metric-definitions` + ), + setMethod('GET') + ); +}; + +export const getJWEToken = (data: JWETokenPayLoad, serviceType: string) => + Request( + setURL( + `${API_ROOT}/monitor/services/${encodeURIComponent(serviceType)}/token` + ), + setMethod('POST'), + setData(data) + ); diff --git a/packages/api-v4/src/cloudpulse/types.ts b/packages/api-v4/src/cloudpulse/types.ts index b1a8b38f7a2..6a6090501fe 100644 --- a/packages/api-v4/src/cloudpulse/types.ts +++ b/packages/api-v4/src/cloudpulse/types.ts @@ -59,3 +59,31 @@ export interface AclpWidget { label: string; size: number; } + +export interface MetricDefinitions { + data: AvailableMetrics[]; +} + +export interface AvailableMetrics { + label: string; + metric: string; + metric_type: string; + unit: string; + scrape_interval: string; + available_aggregate_functions: string[]; + dimensions: Dimension[]; +} + +export interface Dimension { + label: string; + dimension_label: string; + values: string[]; +} + +export interface JWETokenPayLoad { + resource_id: string[]; +} + +export interface JWEToken{ + token: string; +} diff --git a/packages/manager/.changeset/pr-10676-upcoming-features-1721049757333.md b/packages/manager/.changeset/pr-10676-upcoming-features-1721049757333.md new file mode 100644 index 00000000000..7b69f2ca919 --- /dev/null +++ b/packages/manager/.changeset/pr-10676-upcoming-features-1721049757333.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Added widget component in the UI for metrics data ([#10676](https://github.com/linode/manager/pull/10676)) diff --git a/packages/manager/src/assets/icons/entityIcons/cv_overview.svg b/packages/manager/src/assets/icons/entityIcons/cv_overview.svg new file mode 100644 index 00000000000..e29996b4104 --- /dev/null +++ b/packages/manager/src/assets/icons/entityIcons/cv_overview.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/manager/src/features/CloudPulse/CloudPulseTabs.tsx b/packages/manager/src/features/CloudPulse/CloudPulseTabs.tsx index a6562db5204..ddcb223c17f 100644 --- a/packages/manager/src/features/CloudPulse/CloudPulseTabs.tsx +++ b/packages/manager/src/features/CloudPulse/CloudPulseTabs.tsx @@ -1,6 +1,6 @@ import { styled } from '@mui/material/styles'; import * as React from 'react'; -import { RouteComponentProps, matchPath } from 'react-router-dom'; +import { matchPath } from 'react-router-dom'; import { SuspenseLoader } from 'src/components/SuspenseLoader'; import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; @@ -8,7 +8,9 @@ import { TabLinkList } from 'src/components/Tabs/TabLinkList'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; -import { DashboardLanding } from './Dashboard/DashboardLanding'; +import { CloudPulseDashboardLanding } from './Dashboard/CloudPulseDashboardLanding'; + +import type { RouteComponentProps } from 'react-router-dom'; type Props = RouteComponentProps<{}>; export const CloudPulseTabs = React.memo((props: Props) => { @@ -40,7 +42,7 @@ export const CloudPulseTabs = React.memo((props: Props) => { }> - + diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx new file mode 100644 index 00000000000..5790a15d0f3 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx @@ -0,0 +1,241 @@ +import { Grid, Paper } from '@mui/material'; +import React from 'react'; + +import CloudPulseIcon from 'src/assets/icons/entityIcons/cv_overview.svg'; +import { CircleProgress } from 'src/components/CircleProgress'; +import { ErrorState } from 'src/components/ErrorState/ErrorState'; +import { Placeholder } from 'src/components/Placeholder/Placeholder'; +import { useCloudPulseDashboardByIdQuery } from 'src/queries/cloudpulse/dashboards'; +import { useResourcesQuery } from 'src/queries/cloudpulse/resources'; +import { + useCloudPulseJWEtokenQuery, + useGetCloudPulseMetricDefinitionsByServiceType, +} from 'src/queries/cloudpulse/services'; + +import { getUserPreferenceObject } from '../Utils/UserPreference'; +import { createObjectCopy } from '../Utils/utils'; +import { CloudPulseWidget } from '../Widget/CloudPulseWidget'; +import { + all_interval_options, + getInSeconds, + getIntervalIndex, +} from '../Widget/components/CloudPulseIntervalSelect'; + +import type { CloudPulseWidgetProperties } from '../Widget/CloudPulseWidget'; +import type { + AvailableMetrics, + Dashboard, + JWETokenPayLoad, + TimeDuration, + Widgets, +} from '@linode/api-v4'; + +export interface DashboardProperties { + /** + * Id of the selected dashboard + */ + dashboardId: number; + + /** + * time duration to fetch the metrics data in this widget + */ + duration: TimeDuration; + + /** + * optional timestamp to pass as react query param to forcefully re-fetch data + */ + manualRefreshTimeStamp?: number | undefined; + + /** + * Selected region for the dashboard + */ + region?: string; + + /** + * Selected resources for the dashboard + */ + resources: string[]; + + /** + * optional flag to check whether changes should be stored in preferences or not (in case this component is reused) + */ + savePref?: boolean; +} + +export const CloudPulseDashboard = (props: DashboardProperties) => { + const { + dashboardId, + duration, + manualRefreshTimeStamp, + resources, + savePref, + } = props; + + const getJweTokenPayload = (): JWETokenPayLoad => { + return { + resource_id: resourceList?.map((resource) => String(resource.id)) ?? [], + }; + }; + + const getCloudPulseGraphProperties = ( + widget: Widgets + ): CloudPulseWidgetProperties => { + const graphProp: CloudPulseWidgetProperties = { + ariaLabel: widget.label, + authToken: '', + availableMetrics: undefined, + duration, + errorLabel: 'Error While Loading Data', + resourceIds: resources, + resources: [], + serviceType: dashboard?.service_type ?? '', + timeStamp: manualRefreshTimeStamp, + unit: widget.unit ?? '%', + widget: { ...widget }, + }; + if (savePref) { + setPreferredWidgetPlan(graphProp.widget); + } + return graphProp; + }; + + const setPreferredWidgetPlan = (widgetObj: Widgets) => { + const widgetPreferences = getUserPreferenceObject().widgets; + const pref = widgetPreferences?.[widgetObj.label]; + if (pref) { + Object.assign(widgetObj, { + aggregate_function: pref.aggregateFunction, + size: pref.size, + time_granularity: { ...pref.timeGranularity }, + }); + } + }; + + const getTimeGranularity = (scrapeInterval: string) => { + const scrapeIntervalValue = getInSeconds(scrapeInterval); + const index = getIntervalIndex(scrapeIntervalValue); + return index < 0 ? all_interval_options[0] : all_interval_options[index]; + }; + + const { + data: dashboard, + isLoading: isDashboardLoading, + } = useCloudPulseDashboardByIdQuery(dashboardId); + + const { + data: resourceList, + isLoading: isResourcesLoading, + } = useResourcesQuery( + Boolean(dashboard?.service_type), + dashboard?.service_type, + {}, + {} + ); + + const { + data: metricDefinitions, + isError: isMetricDefinitionError, + isLoading: isMetricDefinitionLoading, + } = useGetCloudPulseMetricDefinitionsByServiceType( + dashboard?.service_type, + Boolean(dashboard?.service_type) + ); + + const { + data: jweToken, + isError: isJweTokenError, + } = useCloudPulseJWEtokenQuery( + dashboard?.service_type, + getJweTokenPayload(), + Boolean(resourceList) + ); + + if (isJweTokenError) { + return ( + + + + ); + } + + if (isMetricDefinitionLoading || isDashboardLoading || isResourcesLoading) { + return ; + } + + if (isMetricDefinitionError) { + return ; + } + + const RenderWidgets = () => { + if (!dashboard || Boolean(dashboard.widgets?.length)) { + return renderPlaceHolder( + 'No visualizations are available at this moment. Create Dashboards to list here.' + ); + } + + if ( + !dashboard.service_type || + !Boolean(resources.length > 0) || + !jweToken?.token || + !Boolean(resourceList?.length) + ) { + return renderPlaceHolder( + 'Select Dashboard, Region and Resource to visualize metrics' + ); + } + + // maintain a copy + const newDashboard: Dashboard = createObjectCopy(dashboard)!; + return ( + + {{ ...newDashboard }.widgets.map((widget, index) => { + // check if widget metric definition is available or not + if (widget) { + // find the metric defintion of the widget label + const availMetrics = metricDefinitions?.data.find( + (availMetrics: AvailableMetrics) => + widget.label === availMetrics.label + ); + const cloudPulseWidgetProperties = getCloudPulseGraphProperties({ + ...widget, + }); + + // metric definition is available but time_granularity is not present + if ( + availMetrics && + !cloudPulseWidgetProperties.widget.time_granularity + ) { + cloudPulseWidgetProperties.widget.time_granularity = getTimeGranularity( + availMetrics.scrape_interval + ); + } + return ( + + ); + } else { + return ; + } + })} + + ); + }; + + const renderPlaceHolder = (subtitle: string) => { + return ( + + + + + + ); + }; + + return ; +}; diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx new file mode 100644 index 00000000000..167160d62c1 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx @@ -0,0 +1,37 @@ +import { Grid, Paper } from '@mui/material'; +import * as React from 'react'; + +import { CircleProgress } from 'src/components/CircleProgress'; + +import { GlobalFilters } from '../Overview/GlobalFilters'; +import { useLoadUserPreferences } from '../Utils/UserPreference'; +import { CloudPulseDashboard } from './CloudPulseDashboard'; + +import type { FiltersObject } from '../Overview/GlobalFilters'; +import type { TimeDuration } from '@linode/api-v4'; + +export const CloudPulseDashboardLanding = () => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + const onFilterChange = React.useCallback((_filters: FiltersObject) => {}, []); + const { isLoading } = useLoadUserPreferences(); + + if (isLoading) { + return ; + } + return ( + + + + + + + + + ); +}; diff --git a/packages/manager/src/features/CloudPulse/Dashboard/DashboardLanding.tsx b/packages/manager/src/features/CloudPulse/Dashboard/DashboardLanding.tsx deleted file mode 100644 index f1cc664825b..00000000000 --- a/packages/manager/src/features/CloudPulse/Dashboard/DashboardLanding.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Paper } from '@mui/material'; -import * as React from 'react'; - -import { CircleProgress } from 'src/components/CircleProgress'; - -import { GlobalFilters } from '../Overview/GlobalFilters'; -import { useLoadUserPreferences } from '../Utils/UserPreference'; - -import type { FiltersObject } from '../Overview/GlobalFilters'; - -export const DashboardLanding = () => { - // eslint-disable-next-line @typescript-eslint/no-empty-function - const onFilterChange = React.useCallback((_filters: FiltersObject) => {}, []); - const { isLoading } = useLoadUserPreferences(); - - if (isLoading) { - return ; - } - - return ( - -
-
- -
-
-
- ); -}; diff --git a/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.tsx b/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.tsx index 302718c518f..8d89447db6c 100644 --- a/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.tsx +++ b/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.tsx @@ -1,8 +1,10 @@ import { Dashboard } from '@linode/api-v4'; +import { IconButton, Tooltip } from '@mui/material'; import { styled } from '@mui/material/styles'; import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; +import Reload from 'src/assets/icons/reload.svg'; import { WithStartAndEnd } from 'src/features/Longview/request.types'; import { CloudPulseDashboardSelect } from '../shared/CloudPulseDashboardSelect'; @@ -81,6 +83,8 @@ export const GlobalFilters = React.memo((props: GlobalFilterProperties) => { [] ); + const handleGlobalRefresh = React.useCallback(() => {}, []); + return ( @@ -110,6 +114,13 @@ export const GlobalFilters = React.memo((props: GlobalFilterProperties) => { label="Select Time Range" /> + + + + + + + ); @@ -146,3 +157,14 @@ const itemSpacing = { boxSizing: 'border-box', margin: '0', }; + +const StyledReload = styled(Reload, { label: 'StyledReload' })(({ theme }) => ({ + '&:active': { + color: `${theme.palette.success}`, + }, + '&:hover': { + cursor: 'pointer', + }, + height: '27px', + width: '27px', +})); diff --git a/packages/manager/src/features/CloudPulse/Utils/UserPreference.ts b/packages/manager/src/features/CloudPulse/Utils/UserPreference.ts index 956f7a2f1e4..175a0d0f2e8 100644 --- a/packages/manager/src/features/CloudPulse/Utils/UserPreference.ts +++ b/packages/manager/src/features/CloudPulse/Utils/UserPreference.ts @@ -3,7 +3,7 @@ import { usePreferences, } from 'src/queries/profile/preferences'; -import type { AclpConfig } from '@linode/api-v4'; +import type { AclpConfig, AclpWidget } from '@linode/api-v4'; let userPreference: AclpConfig; let timerId: ReturnType; @@ -47,6 +47,27 @@ export const updateGlobalFilterPreference = (data: {}) => { debounce(userPreference); }; +export const updateWidgetPreference = ( + label: string, + data: Partial +) => { + if (!userPreference) { + userPreference = {} as AclpConfig; + } + + if (!userPreference.widgets) { + userPreference.widgets = {}; + } + + userPreference.widgets[label] = { + ...userPreference.widgets[label], + label, + ...data, + }; + + debounce(userPreference); +}; + // to avoid frequent preference update calls within 500 ms interval const debounce = (updatedData: AclpConfig) => { if (timerId) { diff --git a/packages/manager/src/features/CloudPulse/utils.ts b/packages/manager/src/features/CloudPulse/Utils/utils.ts similarity index 57% rename from packages/manager/src/features/CloudPulse/utils.ts rename to packages/manager/src/features/CloudPulse/Utils/utils.ts index 62403c09d09..eb18278833a 100644 --- a/packages/manager/src/features/CloudPulse/utils.ts +++ b/packages/manager/src/features/CloudPulse/Utils/utils.ts @@ -18,3 +18,23 @@ export const useIsACLPEnabled = (): { return { isACLPEnabled }; }; + +export const convertStringToCamelCasesWithSpaces = ( + nonFormattedString: string +): string => { + return nonFormattedString + ?.split(' ') + .map((text) => text.charAt(0).toUpperCase() + text.slice(1)) + .join(' '); +}; + +export const createObjectCopy = (object: T): T | null => { + if (!object) { + return null; + } + try { + return JSON.parse(JSON.stringify(object)); + } catch (e) { + return null; + } +}; diff --git a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx new file mode 100644 index 00000000000..b89f55891d2 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx @@ -0,0 +1,248 @@ +import { Box, Grid, Paper, Stack, Typography } from '@mui/material'; +import React from 'react'; + +import { Divider } from 'src/components/Divider'; +import { LineGraph } from 'src/components/LineGraph/LineGraph'; +import { useProfile } from 'src/queries/profile/profile'; + +import { AGGREGATE_FUNCTION, SIZE, TIME_GRANULARITY } from '../Utils/constants'; +import { + getUserPreferenceObject, + updateWidgetPreference, +} from '../Utils/UserPreference'; +import { convertStringToCamelCasesWithSpaces } from '../Utils/utils'; +import { CloudPulseAggregateFunction } from './components/CloudPulseAggregateFunction'; +import { CloudPulseIntervalSelect } from './components/CloudPulseIntervalSelect'; +import { ZoomIcon } from './components/Zoomer'; + +import type { CloudPulseResources } from '../shared/CloudPulseResourcesSelect'; +import type { + AvailableMetrics, + TimeDuration, + TimeGranularity, +} from '@linode/api-v4'; +import type { Widgets } from '@linode/api-v4'; + +export interface CloudPulseWidgetProperties { + /** + * Aria label for this widget + */ + ariaLabel?: string; + + /** + * token to fetch metrics data + */ + authToken: string; + + /** + * metrics defined of this widget + */ + availableMetrics: AvailableMetrics | undefined; + + /** + * time duration to fetch the metrics data in this widget + */ + duration: TimeDuration; + + /** + * Any error to be shown in this widget + */ + errorLabel?: string; + + /** + * resources ids selected by user to show metrics for + */ + resourceIds: string[]; + + /** + * List of resources available of selected service type + */ + resources: CloudPulseResources[]; + + /** + * optional flag to check whether changes should be stored in preferences or not (in case this component is reused) + */ + savePref?: boolean; + + /** + * Service type selected by user + */ + serviceType: string; + + /** + * optional timestamp to pass as react query param to forcefully re-fetch data + */ + timeStamp?: number; + + /** + * this should come from dashboard, which maintains map for service types in a separate API call + */ + unit: string; + + /** + * color index to be selected from available them if not theme is provided by user + */ + useColorIndex?: number; + + /** + * this comes from dashboard, has inbuilt metrics, agg_func,group_by,filters,gridsize etc , also helpful in publishing any changes + */ + widget: Widgets; +} + +export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { + const { data: profile } = useProfile(); + + const timezone = profile?.timezone ?? 'US/Eastern'; + + const [widget, setWidget] = React.useState({ ...props.widget }); + + const { availableMetrics, savePref } = props; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [today, _] = React.useState(false); // Temporarily disabled eslint for this line. Will be removed in future PRs + + /** + * + * @param zoomInValue: True if zoom in clicked & False if zoom out icon clicked + */ + const handleZoomToggle = React.useCallback((zoomInValue: boolean) => { + if (savePref) { + updateWidgetPreference(widget.label, { + [SIZE]: zoomInValue ? 12 : 6, + }); + } + + setWidget((currentWidget) => { + return { + ...currentWidget, + size: zoomInValue ? 12 : 6, + }; + }); + }, []); + + /** + * + * @param aggregateValue: aggregate function select from AggregateFunction component + */ + const handleAggregateFunctionChange = React.useCallback( + (aggregateValue: string) => { + // To avoid updation if user again selected the currently selected value from drop down. + if (aggregateValue !== widget.aggregate_function) { + if (savePref) { + updateWidgetPreference(widget.label, { + [AGGREGATE_FUNCTION]: aggregateValue, + }); + } + + setWidget((currentWidget) => { + return { + ...currentWidget, + aggregate_function: aggregateValue, + }; + }); + } + }, + [] + ); + + /** + * + * @param intervalValue : TimeGranularity object selected from the interval select + */ + const handleIntervalChange = React.useCallback( + (intervalValue: TimeGranularity) => { + if ( + !widget.time_granularity || + intervalValue.unit !== widget.time_granularity.unit || + intervalValue.value !== widget.time_duration.value + ) { + if (savePref) { + updateWidgetPreference(widget.label, { + [TIME_GRANULARITY]: { ...intervalValue }, + }); + } + + setWidget((currentWidget) => { + return { + ...currentWidget, + time_granularity: { ...intervalValue }, + }; + }); + } + }, + [] + ); + // Update the widget preference if already not present in the preferences + React.useEffect(() => { + if (savePref) { + const widgets = getUserPreferenceObject()?.widgets; + if (!widgets || !widgets[widget.label]) { + updateWidgetPreference(widget.label, { + [AGGREGATE_FUNCTION]: widget.aggregate_function, + [SIZE]: widget.size, + [TIME_GRANULARITY]: widget.time_granularity, + }); + } + } + }, []); + + return ( + + + + + + {convertStringToCamelCasesWithSpaces(widget.label)}{' '} + + + {availableMetrics?.scrape_interval && ( + + )} + {Boolean( + availableMetrics?.available_aggregate_functions?.length + ) && ( + + )} + + + + + + + + + + + + + ); +}; diff --git a/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseAggregateFunction.test.tsx b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseAggregateFunction.test.tsx new file mode 100644 index 00000000000..e40a09c699e --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseAggregateFunction.test.tsx @@ -0,0 +1,39 @@ +import { fireEvent, screen } from '@testing-library/react'; +import React from 'react'; + +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { CloudPulseAggregateFunction } from './CloudPulseAggregateFunction'; + +import type { AggregateFunctionProperties } from './CloudPulseAggregateFunction'; + +const aggregateFunctionChange = (_selectedAggregateFunction: string) => {}; +const availableAggregateFunctions = ['max', 'min', 'avg']; +const defaultAggregateFunction = 'avg'; + +const props: AggregateFunctionProperties = { + availableAggregateFunctions, + defaultAggregateFunction, + onAggregateFuncChange: aggregateFunctionChange, +}; + +describe('Cloud Pulse Aggregate Function', () => { + it('should check for the selected value in aggregate function dropdown', () => { + const { getByRole } = renderWithTheme( + + ); + + const dropdown = getByRole('combobox'); + + expect(dropdown).toHaveAttribute('value', defaultAggregateFunction); + }); + + it('should select the aggregate function on click', () => { + renderWithTheme(); + + fireEvent.click(screen.getByRole('button', { name: 'Open' })); + fireEvent.click(screen.getByRole('option', { name: 'min' })); + + expect(screen.getByRole('combobox')).toHaveAttribute('value', 'min'); + }); +}); diff --git a/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseAggregateFunction.tsx b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseAggregateFunction.tsx new file mode 100644 index 00000000000..ca63afef004 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseAggregateFunction.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; + +export interface AggregateFunctionProperties { + /** + * List of aggregate functions available to display + */ + availableAggregateFunctions: string[]; + + /** + * Default aggregate function to be selected + */ + defaultAggregateFunction?: string | undefined; + + /** + * Function to be triggered on aggregate function changed from dropdown + */ + onAggregateFuncChange: any; +} + +export const CloudPulseAggregateFunction = React.memo( + (props: AggregateFunctionProperties) => { + // Convert list of availableAggregateFunc into a proper response structure accepted by Autocomplete component + const availableAggregateFunc = props.availableAggregateFunctions?.map( + (aggrFunc) => { + return { + label: aggrFunc, + value: aggrFunc, + }; + } + ); + + const defaultAggregateFunc = + availableAggregateFunc.find( + (obj) => obj.label === props.defaultAggregateFunction + ) || props.availableAggregateFunctions[0]; + + return ( + { + return option.label == value.label; + }} + onChange={(_: any, selectedAggregateFunc: any) => { + props.onAggregateFuncChange(selectedAggregateFunc.label); + }} + defaultValue={defaultAggregateFunc} + disableClearable + fullWidth={false} + label="" + noMarginTop={true} + options={availableAggregateFunc} + sx={{ width: '100%' }} + /> + ); + } +); diff --git a/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.test.tsx b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.test.tsx new file mode 100644 index 00000000000..96023a33da8 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.test.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { CloudPulseIntervalSelect } from './CloudPulseIntervalSelect'; + +import type { TimeGranularity } from '@linode/api-v4'; + +describe('Interval select component', () => { + const intervalSelectionChange = (_selectedInterval: TimeGranularity) => {}; + + it('should check for the selected value in interval select dropdown', () => { + const scrape_interval = '30s'; + const default_interval = { unit: 'min', value: 5 }; + + const { getByRole } = renderWithTheme( + + ); + + const dropdown = getByRole('combobox'); + + expect(dropdown).toHaveAttribute('value', '5 min'); + }); +}); diff --git a/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.tsx b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.tsx new file mode 100644 index 00000000000..01aef34b1ee --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.tsx @@ -0,0 +1,132 @@ +import React from 'react'; + +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; + +import type { TimeGranularity } from '@linode/api-v4'; + +export interface IntervalSelectProperties { + /** + * Default time granularity to be selected + */ + default_interval?: TimeGranularity | undefined; + + /** + * Function to be triggered on aggregate function changed from dropdown + */ + onIntervalChange: any; + + /** + * scrape intervalto filter out minimum time granularity + */ + scrape_interval: string; +} + +export const getInSeconds = (interval: string) => { + if (interval.endsWith('s')) { + return Number(interval.slice(0, -1)); + } + if (interval.endsWith('m')) { + return Number(interval.slice(0, -1)) * 60; + } + if (interval.endsWith('h')) { + return Number(interval.slice(0, -1)) * 3600; + } + if (interval.endsWith('d')) { + return Number(interval.slice(0, -1)) * 86400; + } + return 0; + // month and year cases to be added if required +}; + +// Intervals must be in ascending order here +export const all_interval_options = [ + { + label: '1 min', + unit: 'min', + value: 1, + }, + { + label: '5 min', + unit: 'min', + value: 5, + }, + { + label: '1 hr', + unit: 'hr', + value: 1, + }, + { + label: '1 day', + unit: 'day', + value: 1, + }, +]; + +const autoIntervalOption = { + label: 'Auto', + unit: 'Auto', + value: -1, +}; + +export const getIntervalIndex = (scrapeIntervalValue: number) => { + return all_interval_options.findIndex( + (interval) => + scrapeIntervalValue <= + getInSeconds(String(interval.value) + interval.unit.slice(0, 1)) + ); +}; + +export const CloudPulseIntervalSelect = React.memo( + (props: IntervalSelectProperties) => { + const scrapeIntervalValue = getInSeconds(props.scrape_interval); + + const firstIntervalIndex = getIntervalIndex(scrapeIntervalValue); + + // all intervals displayed if srape interval > highest available interval. Error handling done by api + const available_interval_options = + firstIntervalIndex < 0 + ? all_interval_options.slice() + : all_interval_options.slice( + firstIntervalIndex, + all_interval_options.length + ); + + let default_interval = + props.default_interval?.unit === 'Auto' + ? autoIntervalOption + : available_interval_options.find( + (obj) => + obj.value === props.default_interval?.value && + obj.unit === props.default_interval?.unit + ); + + if (!default_interval) { + default_interval = autoIntervalOption; + props.onIntervalChange({ + unit: default_interval.unit, + value: default_interval.value, + }); + } + + return ( + { + return option?.value === value?.value && option?.unit === value?.unit; + }} + onChange={(_: any, selectedInterval: any) => { + props.onIntervalChange({ + unit: selectedInterval?.unit, + value: selectedInterval?.value, + }); + }} + defaultValue={{ ...default_interval }} + disableClearable + fullWidth={false} + label="" + noMarginTop={true} + options={[autoIntervalOption, ...available_interval_options]} + sx={{ width: { xs: '100%' } }} + /> + ); + } +); diff --git a/packages/manager/src/features/CloudPulse/Widget/components/Zoomer.test.tsx b/packages/manager/src/features/CloudPulse/Widget/components/Zoomer.test.tsx new file mode 100644 index 00000000000..6b6f327c3be --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Widget/components/Zoomer.test.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { ZoomIcon } from './Zoomer'; + +import type { ZoomIconProperties } from './Zoomer'; + +describe('Cloud Pulse Zoomer', () => { + it('Should render zoomer with zoom-out button', () => { + const props: ZoomIconProperties = { + handleZoomToggle: (_zoomInValue: boolean) => {}, + zoomIn: false, + }; + const { getByTestId } = renderWithTheme(); + + expect(getByTestId('zoom-out')).toBeInTheDocument(); + }), + it('Should render zoomer with zoom-in button', () => { + const props: ZoomIconProperties = { + handleZoomToggle: (_zoomInValue: boolean) => {}, + zoomIn: true, + }; + const { getByTestId } = renderWithTheme(); + + expect(getByTestId('zoom-in')).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/src/features/CloudPulse/Widget/components/Zoomer.tsx b/packages/manager/src/features/CloudPulse/Widget/components/Zoomer.tsx new file mode 100644 index 00000000000..6ed093ed513 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Widget/components/Zoomer.tsx @@ -0,0 +1,37 @@ +import ZoomInMap from '@mui/icons-material/ZoomInMap'; +import ZoomOutMap from '@mui/icons-material/ZoomOutMap'; +import * as React from 'react'; + +export interface ZoomIconProperties { + className?: string; + handleZoomToggle: (zoomIn: boolean) => void; + zoomIn: boolean; +} + +export const ZoomIcon = React.memo((props: ZoomIconProperties) => { + const handleClick = (needZoomIn: boolean) => { + props.handleZoomToggle(needZoomIn); + }; + + const ToggleZoomer = () => { + if (props.zoomIn) { + return ( + handleClick(false)} + style={{ color: 'grey', fontSize: 'x-large' }} + /> + ); + } + + return ( + handleClick(true)} + style={{ color: 'grey', fontSize: 'x-large' }} + /> + ); + }; + + return ; +}); diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.test.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.test.tsx index 09d826f6351..c3b58a27ec4 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.test.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.test.tsx @@ -16,18 +16,18 @@ const props: CloudPulseDashboardSelectProps = { }; const queryMocks = vi.hoisted(() => ({ - useCloudViewDashboardsQuery: vi.fn().mockReturnValue({}), + useCloudPulseDashboardsQuery: vi.fn().mockReturnValue({}), })); vi.mock('src/queries/cloudpulse/dashboards', async () => { const actual = await vi.importActual('src/queries/cloudpulse/dashboards'); return { ...actual, - useCloudViewDashboardsQuery: queryMocks.useCloudViewDashboardsQuery, + useCloudPulseDashboardsQuery: queryMocks.useCloudPulseDashboardsQuery, }; }); -queryMocks.useCloudViewDashboardsQuery.mockReturnValue({ +queryMocks.useCloudPulseDashboardsQuery.mockReturnValue({ data: { data: [ { @@ -51,7 +51,7 @@ describe('CloudPulse Dashboard select', () => { ); - expect(getByTestId('cloudview-dashboard-select')).toBeInTheDocument(); + expect(getByTestId('cloudpulse-dashboard-select')).toBeInTheDocument(); expect(getByPlaceholderText('Select a Dashboard')).toBeInTheDocument(); }), it('Should render dashboard select component with data', () => { diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.tsx index f8efd3abfa5..f7eaa1c3dc2 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { Box } from 'src/components/Box'; import { Typography } from 'src/components/Typography'; -import { useCloudViewDashboardsQuery } from 'src/queries/cloudpulse/dashboards'; +import { useCloudPulseDashboardsQuery } from 'src/queries/cloudpulse/dashboards'; import { DASHBOARD_ID, REGION, RESOURCES } from '../Utils/constants'; import { @@ -25,7 +25,7 @@ export const CloudPulseDashboardSelect = React.memo( data: dashboardsList, error, isLoading, - } = useCloudViewDashboardsQuery(true); // Fetch the list of dashboards + } = useCloudPulseDashboardsQuery(true); // Fetch the list of dashboards const [ selectedDashboard, @@ -85,7 +85,7 @@ export const CloudPulseDashboardSelect = React.memo( )} autoHighlight clearOnBlur - data-testid="cloudview-dashboard-select" + data-testid="cloudpulse-dashboard-select" disabled={!dashboardsList} errorText={errorText} fullWidth diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.test.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.test.tsx index 7779224595f..07de1dea75d 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.test.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.test.tsx @@ -13,7 +13,7 @@ const props: CloudPulseRegionSelectProps = { selectedDashboard: undefined, }; -describe('CloudViewRegionSelect', () => { +describe('CloudPulseRegionSelect', () => { vi.spyOn(regions, 'useRegionsQuery').mockReturnValue({ data: Array(), } as ReturnType); diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index c3de740fb94..b26a45e445c 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -103,8 +103,8 @@ import { pickRandom } from 'src/utilities/random'; import { getStorage } from 'src/utilities/storage'; import type { - NotificationType, CreateObjectStorageKeyPayload, + NotificationType, SecurityQuestionsPayload, TokenRequest, UpdateImageRegionsPayload, @@ -2431,6 +2431,185 @@ export const handlers = [ return HttpResponse.json(response); }), + http.get('*/v4/monitor/services/:serviceType/metric-definitions', () => { + const response = { + data: [ + { + available_aggregate_functions: ['min', 'max', 'avg'], + dimensions: [ + { + dim_label: 'cpu', + label: 'CPU name', + values: null, + }, + { + dim_label: 'state', + label: 'State of CPU', + values: [ + 'user', + 'system', + 'idle', + 'interrupt', + 'nice', + 'softirq', + 'steal', + 'wait', + ], + }, + { + dim_label: 'LINODE_ID', + label: 'Linode ID', + values: null, + }, + ], + label: 'CPU utilization', + metric: 'system_cpu_utilization_percent', + metric_type: 'gauge', + scrape_interval: '2m', + unit: 'percent', + }, + { + available_aggregate_functions: ['min', 'max', 'avg', 'sum'], + dimensions: [ + { + dim_label: 'state', + label: 'State of memory', + values: [ + 'used', + 'free', + 'buffered', + 'cached', + 'slab_reclaimable', + 'slab_unreclaimable', + ], + }, + { + dim_label: 'LINODE_ID', + label: 'Linode ID', + values: null, + }, + ], + label: 'Memory Usage', + metric: 'system_memory_usage_by_resource', + metric_type: 'gauge', + scrape_interval: '30s', + unit: 'byte', + }, + { + available_aggregate_functions: ['min', 'max', 'avg', 'sum'], + dimensions: [ + { + dim_label: 'device', + label: 'Device name', + values: ['lo', 'eth0'], + }, + { + dim_label: 'direction', + label: 'Direction of network transfer', + values: ['transmit', 'receive'], + }, + { + dim_label: 'LINODE_ID', + label: 'Linode ID', + values: null, + }, + ], + label: 'Network Traffic', + metric: 'system_network_io_by_resource', + metric_type: 'counter', + scrape_interval: '30s', + unit: 'byte', + }, + { + available_aggregate_functions: ['min', 'max', 'avg', 'sum'], + dimensions: [ + { + dim_label: 'device', + label: 'Device name', + values: ['loop0', 'sda', 'sdb'], + }, + { + dim_label: 'direction', + label: 'Operation direction', + values: ['read', 'write'], + }, + { + dim_label: 'LINODE_ID', + label: 'Linode ID', + values: null, + }, + ], + label: 'Disk I/O', + metric: 'system_disk_OPS_total', + metric_type: 'counter', + scrape_interval: '30s', + unit: 'ops_per_second', + }, + ], + }; + + return HttpResponse.json(response); + }), + http.post('*/v4/monitor/services/:serviceType/token', () => { + const response = { + token: 'eyJhbGciOiAiZGlyIiwgImVuYyI6ICJBMTI4Q0JDLUhTMjU2IiwgImtpZCI6ID', + }; + return HttpResponse.json(response); + }), + + http.get('*/v4/monitor/dashboards/:id', () => { + const response = { + created: '2024-04-29T17:09:29', + id: 1, + label: 'Linode Service I/O Statistics', + service_type: 'linode', + type: 'standard', + updated: null, + widgets: [ + { + aggregate_function: 'avg', + chart_type: 'area', + color: 'blue', + label: 'CPU utilization', + metric: 'system_cpu_utilization_percent', + size: 12, + unit: '%', + y_label: 'system_cpu_utilization_ratio', + }, + { + aggregate_function: 'avg', + chart_type: 'area', + color: 'red', + label: 'Memory Usage', + metric: 'system_memory_usage_by_resource', + size: 12, + unit: 'Bytes', + y_label: 'system_memory_usage_bytes', + }, + { + aggregate_function: 'avg', + chart_type: 'area', + color: 'green', + label: 'Network Traffic', + metric: 'system_network_io_by_resource', + size: 6, + unit: 'Bytes', + y_label: 'system_network_io_bytes_total', + }, + { + aggregate_function: 'avg', + chart_type: 'area', + color: 'yellow', + label: 'Disk I/O', + metric: 'system_disk_OPS_total', + size: 6, + unit: 'OPS', + y_label: 'system_disk_operations_total', + }, + ], + }; + return HttpResponse.json(response); + }), ...entityTransfers, ...statusPage, ...databases, diff --git a/packages/manager/src/queries/cloudpulse/dashboards.ts b/packages/manager/src/queries/cloudpulse/dashboards.ts index 94a9c1c5ca8..737bd6ac8e7 100644 --- a/packages/manager/src/queries/cloudpulse/dashboards.ts +++ b/packages/manager/src/queries/cloudpulse/dashboards.ts @@ -1,15 +1,17 @@ -import { Dashboard, getDashboards } from '@linode/api-v4'; -import { APIError, ResourcePage } from '@linode/api-v4/lib/types'; +import { getDashboardById, getDashboards } from '@linode/api-v4'; import { createQueryKeys } from '@lukemorales/query-key-factory'; import { useQuery } from '@tanstack/react-query'; -export const queryKey = 'cloudview-dashboards'; +import type { Dashboard } from '@linode/api-v4'; +import type { APIError, ResourcePage } from '@linode/api-v4/lib/types'; -export const dashboardQueries = createQueryKeys('cloudview-dashboards', { +export const queryKey = 'cloudpulse-dashboards'; + +export const dashboardQueries = createQueryKeys(queryKey, { dashboardById: (dashboardId: number) => ({ contextQueries: { dashboard: { - queryFn: () => {}, // Todo: will be implemented later + queryFn: () => getDashboardById(dashboardId), queryKey: [dashboardId], }, }, @@ -28,9 +30,18 @@ export const dashboardQueries = createQueryKeys('cloudview-dashboards', { }); // Fetch the list of all the dashboard available -export const useCloudViewDashboardsQuery = (enabled: boolean) => { +export const useCloudPulseDashboardsQuery = (enabled: boolean) => { return useQuery, APIError[]>({ ...dashboardQueries.lists._ctx.allDashboards, enabled, }); }; + +export const useCloudPulseDashboardByIdQuery = ( + dashboardId: number | undefined +) => { + return useQuery({ + ...dashboardQueries.dashboardById(dashboardId!)._ctx.dashboard, + enabled: dashboardId !== undefined, + }); +}; diff --git a/packages/manager/src/queries/cloudpulse/services.ts b/packages/manager/src/queries/cloudpulse/services.ts new file mode 100644 index 00000000000..0f727400c0c --- /dev/null +++ b/packages/manager/src/queries/cloudpulse/services.ts @@ -0,0 +1,47 @@ +import { getJWEToken, getMetricDefinitionsByServiceType } from '@linode/api-v4'; +import { createQueryKeys } from '@lukemorales/query-key-factory'; +import { useQuery } from '@tanstack/react-query'; + +import type { + APIError, + JWEToken, + JWETokenPayLoad, + MetricDefinitions, +} from '@linode/api-v4'; + +export const queryKey = 'cloudpulse-services'; +export const serviceTypeKey = 'service-types'; + +const serviceQueries = createQueryKeys(queryKey, { + metricsDefinitons: (serviceType: string | undefined) => ({ + queryFn: () => getMetricDefinitionsByServiceType(serviceType!), + queryKey: [serviceType], + }), + token: (serviceType: string | undefined, request: JWETokenPayLoad) => ({ + queryFn: () => getJWEToken(request, serviceType!), + queryKey: [serviceType], + }), +}); + +export const useGetCloudPulseMetricDefinitionsByServiceType = ( + serviceType: string | undefined, + enabled: boolean +) => { + return useQuery({ + ...serviceQueries.metricsDefinitons(serviceType), + enabled, + }); +}; + +export const useCloudPulseJWEtokenQuery = ( + serviceType: string | undefined, + request: JWETokenPayLoad, + runQuery: boolean +) => { + return useQuery({ + ...serviceQueries.token(serviceType, request), + enabled: runQuery, + keepPreviousData: true, + refetchOnWindowFocus: false, + }); +};