Skip to content

Commit

Permalink
upcoming: [DI-19610] - Added CloudPulse widget to show graph for diff…
Browse files Browse the repository at this point in the history
…erent metrices (linode#10676)

Co-authored-by: Hana Xu <[email protected]>
Co-authored-by: Jaalah Ramos <[email protected]>
Co-authored-by: Jaalah Ramos <[email protected]>
  • Loading branch information
4 people authored Jul 23, 2024
1 parent 1cc9de6 commit 8d1ad97
Show file tree
Hide file tree
Showing 27 changed files with 1,244 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -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))
8 changes: 7 additions & 1 deletion packages/api-v4/src/cloudpulse/dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResourcePage<Dashboard>>(
setURL(`${API_ROOT}/monitor/services/linode/dashboards`),
setMethod('GET')
);

export const getDashboardById = (dashboardId: number) =>
Request<Dashboard>(
setURL(`${API_ROOT}/monitor/dashboards/${encodeURIComponent(dashboardId)}`),
setMethod('GET')
);
2 changes: 2 additions & 0 deletions packages/api-v4/src/cloudpulse/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './types';

export * from './dashboards';

export * from './services';
24 changes: 24 additions & 0 deletions packages/api-v4/src/cloudpulse/services.ts
Original file line number Diff line number Diff line change
@@ -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<Page<MetricDefinitions>>(
setURL(
`${API_ROOT}/monitor/services/${encodeURIComponent(
serviceType
)}/metric-definitions`
),
setMethod('GET')
);
};

export const getJWEToken = (data: JWETokenPayLoad, serviceType: string) =>
Request<JWEToken>(
setURL(
`${API_ROOT}/monitor/services/${encodeURIComponent(serviceType)}/token`
),
setMethod('POST'),
setData(data)
);
28 changes: 28 additions & 0 deletions packages/api-v4/src/cloudpulse/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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))
5 changes: 5 additions & 0 deletions packages/manager/src/assets/icons/entityIcons/cv_overview.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions packages/manager/src/features/CloudPulse/CloudPulseTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
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';
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) => {
Expand Down Expand Up @@ -40,7 +42,7 @@ export const CloudPulseTabs = React.memo((props: Props) => {
<React.Suspense fallback={<SuspenseLoader />}>
<TabPanels>
<SafeTabPanel index={0}>
<DashboardLanding />
<CloudPulseDashboardLanding />
</SafeTabPanel>
</TabPanels>
</React.Suspense>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Grid item xs>
<ErrorState errorText="Failed to get jwe token" />
</Grid>
);
}

if (isMetricDefinitionLoading || isDashboardLoading || isResourcesLoading) {
return <CircleProgress />;
}

if (isMetricDefinitionError) {
return <ErrorState errorText={'Error loading metric definitions'} />;
}

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 (
<Grid columnSpacing={1} container item rowSpacing={2} xs={12}>
{{ ...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 (
<CloudPulseWidget
key={widget.label}
{...cloudPulseWidgetProperties}
authToken={jweToken?.token}
availableMetrics={availMetrics}
resources={resourceList!}
savePref={savePref}
/>
);
} else {
return <React.Fragment key={index}></React.Fragment>;
}
})}
</Grid>
);
};

const renderPlaceHolder = (subtitle: string) => {
return (
<Grid item xs>
<Paper>
<Placeholder icon={CloudPulseIcon} subtitle={subtitle} title="" />
</Paper>
</Grid>
);
};

return <RenderWidgets />;
};
Loading

0 comments on commit 8d1ad97

Please sign in to comment.