Skip to content

Commit

Permalink
Upcoming: [DI-20844] - Tooltip for widget level filters and icons, be…
Browse files Browse the repository at this point in the history
…ta feedbacks and CSS changes for CloudPulse (linode#11062)

* upcoming: [DI-20800] - Tooltip changes

* upcoming: [DI-20800] - Tooltip and publishing the resource selection onClose from Autocomplete

* upcoming: [DI-20800] - Resource Selection close state handling updates

* upcoming: [DI-20800] - Tooltip code refactoring

* upcoming: [DI-20800] - Tooltip code refactoring

* upcoming: [DI-20800] - Global Filters

* upcoming: [DI-20800] - As per dev

* upcoming: [DI-20800] - Code clean up and refactoring

* upcoming: [DI-20800] - UT fixes

* upcoming: [DI-20800] - Code clean up and review comments

* upcoming: [DI-20800] - Mock related changes

* upcoming: [DI-20800] - Code clean up and refactoring

* upcoming: [DI-21254] - Handle unit using reference values

* upcoming: [DI-21254] - Handle unit using reference values

* Update metrics api end point

* Updated non-null assertions

* upcoming: [DI-20800] - Code clean up and refactoring

* upcoming: [DI-20800] - Add Changeset

* upcoming: [DI-20973] - Updated zeroth state message in contextual view

* upcoming: [DI-21359] - Added auto interval option as defaut interval value if pref is not available

* upcoming: [DI-21359] - Updated autoIntervalOptions variable instead of hardcoded value

* upcoming: [DI-20800] - Quick code refactoring

* upcoming: [DI-20800] - We don't need memo here in CloudPulseWidget

* upcoming: [DI-20973] - Updated cloudpulse icon contextual view

* upcoming: [DI-20973] - Updated test cases

* upcoming: [DI-20800] - use theme spacing for height calculation

* upcoming: [DI-20800] - aria-label for global refresh

* upcoming: [DI-20800] - aria-label for zoomers

* upcoming: [DI-20800] - PR review comments initial changes

* upcoming: [DI-20800] - PR review comments

* upcoming: [DI-20800] - PR review comments

* upcoming: [DI-20844] - Remove height

* upcoming: [DI-20844] - Remove comment

* upcoming: [DI-20844] - Using reusable sx and removing styled component

* upcoming: [DI-20844] - Fix eslint and use common tooltip component

* upcoming: [DI-20844] - Remove unused variables

* upcoming: [DI-20844] - Remove arrow

* upcoming: [DI-20844] - Added explaining comments

* upcoming: [DI-20844] - Added UTs for tooltips

* upcoming: [DI-20844] - Updated UT's

* upcoming: [DI-20844] - PR comments

---------

Co-authored-by: vmangalr <vmangalr@akamai.com>
Co-authored-by: nikhagra-akamai <nagrawal@akamai.com>
  • Loading branch information
3 people authored Oct 16, 2024
1 parent 436c010 commit 4e7cb48
Show file tree
Hide file tree
Showing 20 changed files with 232 additions and 146 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add tooltip for widget level filters and icons, address beta demo feedbacks and CSS changes for CloudPulse ([#11062](https://github.com/linode/manager/pull/11062))
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const queryMocks = vi.hoisted(() => ({
}));

const circleProgress = 'circle-progress';
const mandatoryFiltersError = 'Mandatory Filters not Selected';
const mandatoryFiltersError = 'Select filters to visualize metrics.';

vi.mock('src/queries/cloudpulse/dashboards', async () => {
const actual = await vi.importActual('src/queries/cloudpulse/dashboards');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Grid, styled } from '@mui/material';
import { Grid } from '@mui/material';
import React from 'react';

import CloudPulseIcon from 'src/assets/icons/entityIcons/monitor.svg';
import { CircleProgress } from 'src/components/CircleProgress';
import { ErrorState } from 'src/components/ErrorState/ErrorState';
import { Paper } from 'src/components/Paper';
import { Placeholder } from 'src/components/Placeholder/Placeholder';
import { useCloudPulseDashboardByIdQuery } from 'src/queries/cloudpulse/dashboards';

import { CloudPulseDashboardFilterBuilder } from '../shared/CloudPulseDashboardFilterBuilder';
import { CloudPulseErrorPlaceholder } from '../shared/CloudPulseErrorPlaceholder';
import { CloudPulseTimeRangeSelect } from '../shared/CloudPulseTimeRangeSelect';
import { FILTER_CONFIG } from '../Utils/FilterConfig';
import {
Expand Down Expand Up @@ -65,7 +64,7 @@ export const CloudPulseDashboardWithFilters = React.memo(
const renderPlaceHolder = (title: string) => {
return (
<Paper>
<StyledPlaceholder icon={CloudPulseIcon} isEntity title={title} />
<CloudPulseErrorPlaceholder errorMessage={title} />
</Paper>
);
};
Expand Down Expand Up @@ -148,16 +147,9 @@ export const CloudPulseDashboardWithFilters = React.memo(
})}
/>
) : (
renderPlaceHolder('Mandatory Filters not Selected')
renderPlaceHolder('Select filters to visualize metrics.')
)}
</>
);
}
);

// keeping it here to avoid recreating
const StyledPlaceholder = styled(Placeholder, {
label: 'StyledPlaceholder',
})({
flex: 'auto',
});
27 changes: 15 additions & 12 deletions packages/manager/src/features/CloudPulse/Overview/GlobalFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Divider } from 'src/components/Divider';
import { CloudPulseDashboardFilterBuilder } from '../shared/CloudPulseDashboardFilterBuilder';
import { CloudPulseDashboardSelect } from '../shared/CloudPulseDashboardSelect';
import { CloudPulseTimeRangeSelect } from '../shared/CloudPulseTimeRangeSelect';
import { CloudPulseTooltip } from '../shared/CloudPulseTooltip';
import { DASHBOARD_ID, REFRESH, TIME_DURATION } from '../Utils/constants';
import { useAclpPreference } from '../Utils/UserPreference';

Expand Down Expand Up @@ -109,18 +110,20 @@ export const GlobalFilters = React.memo((props: GlobalFilterProperties) => {
label="Select Time Range"
savePreferences
/>
<IconButton
sx={{
marginBlockEnd: 'auto',
}}
data-testid="global-refresh"
aria-label="Refresh Dashboard Metrics"
disabled={!selectedDashboard}
onClick={handleGlobalRefresh}
size="small"
>
<StyledReload />
</IconButton>
<CloudPulseTooltip placement="bottom-end" title="Refresh">
<IconButton
sx={{
marginBlockEnd: 'auto',
}}
aria-label="Refresh Dashboard Metrics"
data-testid="global-refresh"
disabled={!selectedDashboard}
onClick={handleGlobalRefresh}
size="small"
>
<StyledReload />
</IconButton>
</CloudPulseTooltip>
</Grid>
</Grid>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { styled } from '@mui/material';

import { Autocomplete } from 'src/components/Autocomplete/Autocomplete';
import { isToday } from 'src/utilities/isToday';
import { getMetrics } from 'src/utilities/statMetrics';

Expand All @@ -24,6 +21,7 @@ import type {
TimeDuration,
Widgets,
} from '@linode/api-v4';
import type { Theme } from '@mui/material';
import type { DataSet } from 'src/components/LineGraph/LineGraph';
import type { CloudPulseResourceTypeMapFlag, FlagSet } from 'src/featureFlags';

Expand Down Expand Up @@ -336,16 +334,16 @@ export const isDataEmpty = (data: DataSet[]): boolean => {
};

/**
* Returns an autocomplete with updated styles according to UX, this will be used at widget level
*
* @param theme mui theme
* @returns The style needed for widget level autocomplete filters
*/
export const StyledWidgetAutocomplete = styled(Autocomplete, {
label: 'StyledAutocomplete',
})(({ theme }) => ({
export const getAutocompleteWidgetStyles = (theme: Theme) => ({
'&& .MuiFormControl-root': {
minWidth: '90px',
[theme.breakpoints.down('sm')]: {
width: '100%', // 100% width for xs and small screens
},
width: '90px',
},
}));
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
usePreferences,
} from 'src/queries/profile/preferences';

import { DASHBOARD_ID, TIME_DURATION, WIDGETS } from './constants';
import { DASHBOARD_ID, WIDGETS } from './constants';

import type { AclpConfig, AclpWidget } from '@linode/api-v4';

Expand Down Expand Up @@ -37,7 +37,6 @@ export const useAclpPreference = (): AclpPreferenceObject => {
if (keys.includes(DASHBOARD_ID)) {
currentPreferences = {
...data,
[TIME_DURATION]: currentPreferences[TIME_DURATION],
[WIDGETS]: {},
};
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import {
} from '../Utils/CloudPulseWidgetUtils';
import { AGGREGATE_FUNCTION, SIZE, TIME_GRANULARITY } from '../Utils/constants';
import { constructAdditionalRequestFilters } from '../Utils/FilterBuilder';
import { convertValueToUnit, formatToolTip } from '../Utils/unitConversion';
import {
convertValueToUnit,
formatToolTip,
generateCurrentUnit,
} from '../Utils/unitConversion';
import { useAclpPreference } from '../Utils/UserPreference';
import { convertStringToCamelCasesWithSpaces } from '../Utils/utils';
import { CloudPulseAggregateFunction } from './components/CloudPulseAggregateFunction';
Expand Down Expand Up @@ -140,6 +144,7 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
widget: widgetProp,
} = props;
const flags = useFlags();
const scaledWidgetUnit = React.useRef(generateCurrentUnit(unit));

const jweTokenExpiryError = 'Token expired';

Expand Down Expand Up @@ -237,8 +242,6 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {

let legendRows: LegendRow[] = [];
let today: boolean = false;

let currentUnit = unit;
if (!isLoading && metricsList) {
const generatedData = generateGraphData({
flags,
Expand All @@ -255,7 +258,7 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
data = generatedData.dimensions;
legendRows = generatedData.legendRowsData;
today = generatedData.today;
currentUnit = generatedData.unit;
scaledWidgetUnit.current = generatedData.unit; // here state doesn't matter, as this is always the latest re-render
}

const metricsApiCallError = error?.[0]?.reason;
Expand All @@ -276,7 +279,8 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
padding={1}
>
<Typography marginLeft={1} variant="h2">
{convertStringToCamelCasesWithSpaces(widget.label)} ({currentUnit}
{convertStringToCamelCasesWithSpaces(widget.label)} (
{scaledWidgetUnit.current}
{unit.endsWith('ps') ? '/s' : ''})
</Typography>
<Stack
Expand Down Expand Up @@ -319,12 +323,14 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
? metricsApiCallError ?? 'Error while rendering graph'
: undefined
}
formatData={(data: number) =>
convertValueToUnit(data, scaledWidgetUnit.current)
}
legendRows={
legendRows && legendRows.length > 0 ? legendRows : undefined
}
ariaLabel={ariaLabel ? ariaLabel : ''}
data={data}
formatData={(data: number) => convertValueToUnit(data, currentUnit)}
formatTooltip={(value: number) => formatToolTip(value, unit)}
gridSize={widget.size}
loading={isLoading || metricsApiCallError === jweTokenExpiryError} // keep loading until we fetch the refresh token
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { createObjectCopy } from '../Utils/utils';
import { CloudPulseWidget } from './CloudPulseWidget';
import {
allIntervalOptions,
autoIntervalOption,
getInSeconds,
getIntervalIndex,
} from './components/CloudPulseIntervalSelect';
Expand Down Expand Up @@ -80,7 +81,7 @@ export const RenderWidgets = React.memo(
serviceType: dashboard?.service_type ?? '',
timeStamp: manualRefreshTimeStamp,
unit: widget.unit ?? '%',
widget: { ...widget },
widget: { ...widget, time_granularity: autoIntervalOption },
};
if (savePref) {
graphProp.widget = setPreferredWidgetPlan(graphProp.widget);
Expand All @@ -104,17 +105,13 @@ export const RenderWidgets = React.memo(
pref.aggregateFunction ?? widgetObj.aggregate_function,
size: pref.size ?? widgetObj.size,
time_granularity: {
...(pref.timeGranularity ?? widgetObj.time_granularity),
...(pref.timeGranularity ?? autoIntervalOption),
},
};
} else {
return {
...widgetObj,
time_granularity: {
label: 'Auto',
unit: 'Auto',
value: -1,
},
time_granularity: autoIntervalOption,
};
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,42 @@ import React from 'react';

import { renderWithTheme } from 'src/utilities/testHelpers';

import { convertStringToCamelCasesWithSpaces } from '../../Utils/utils';
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,
onAggregateFuncChange: vi.fn(),
};

describe('Cloud Pulse Aggregate Function', () => {
it('should check for the selected value in aggregate function dropdown', () => {
const { getByRole } = renderWithTheme(
const { getByRole, getByTestId } = renderWithTheme(
<CloudPulseAggregateFunction {...props} />
);

const dropdown = getByRole('combobox');

expect(dropdown).toHaveAttribute('value', defaultAggregateFunction);
expect(dropdown).toHaveAttribute(
'value',
convertStringToCamelCasesWithSpaces(defaultAggregateFunction)
);

expect(getByTestId('Aggregation function')).toBeInTheDocument(); // test id for tooltip
});

it('should select the aggregate function on click', () => {
renderWithTheme(<CloudPulseAggregateFunction {...props} />);

fireEvent.click(screen.getByRole('button', { name: 'Open' }));
fireEvent.click(screen.getByRole('option', { name: 'min' }));
fireEvent.click(screen.getByRole('option', { name: 'Min' }));

expect(screen.getByRole('combobox')).toHaveAttribute('value', 'min');
expect(screen.getByRole('combobox')).toHaveAttribute('value', 'Min');
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React from 'react';

import { StyledWidgetAutocomplete } from '../../Utils/CloudPulseWidgetUtils';
import { Autocomplete } from 'src/components/Autocomplete/Autocomplete';

import { CloudPulseTooltip } from '../../shared/CloudPulseTooltip';
import { getAutocompleteWidgetStyles } from '../../Utils/CloudPulseWidgetUtils';
import { convertStringToCamelCasesWithSpaces } from '../../Utils/utils';

export interface AggregateFunctionProperties {
/**
Expand Down Expand Up @@ -41,6 +45,7 @@ export const CloudPulseAggregateFunction = React.memo(
};
}
);

const defaultValue =
availableAggregateFunc.find(
(obj) => obj.label === defaultAggregateFunction
Expand All @@ -52,26 +57,30 @@ export const CloudPulseAggregateFunction = React.memo(
] = React.useState<AggregateFunction>(defaultValue);

return (
<StyledWidgetAutocomplete
isOptionEqualToValue={(option, value) => {
return option.label == value.label;
}}
onChange={(e, selectedAggregateFunc: AggregateFunction) => {
setSelectedAggregateFunction(selectedAggregateFunc);
onAggregateFuncChange(selectedAggregateFunc.label);
}}
textFieldProps={{
hideLabel: true,
}}
autoHighlight
disableClearable
fullWidth={false}
label="Select an Aggregate Function"
noMarginTop={true}
options={availableAggregateFunc}
sx={{ width: '100%' }}
value={selectedAggregateFunction}
/>
<CloudPulseTooltip title={'Aggregation function'}>
<Autocomplete
getOptionLabel={(option) => {
return convertStringToCamelCasesWithSpaces(option.label); // options needed to be display in Caps first
}}
isOptionEqualToValue={(option, value) => {
return option.label === value.label;
}}
onChange={(e, selectedAggregateFunc) => {
setSelectedAggregateFunction(selectedAggregateFunc);
onAggregateFuncChange(selectedAggregateFunc.label);
}}
textFieldProps={{
hideLabel: true,
}}
autoHighlight
disableClearable
label="Select an Aggregate Function"
noMarginTop={true}
options={availableAggregateFunc}
sx={getAutocompleteWidgetStyles}
value={selectedAggregateFunction}
/>
</CloudPulseTooltip>
);
}
);
Loading

0 comments on commit 4e7cb48

Please sign in to comment.