From 22ed724a1dceacf3c3a8e334193c87e973cd44b9 Mon Sep 17 00:00:00 2001 From: vmangalr Date: Wed, 21 Aug 2024 11:15:13 +0530 Subject: [PATCH 01/20] upcoming: [DI-20348] - Changes for CloudPulseCustomSelect components and its Integration --- .../Dashboard/CloudPulseDashboard.tsx | 14 +- .../Dashboard/CloudPulseDashboardLanding.tsx | 11 +- .../CloudPulse/Utils/FilterBuilder.test.ts | 80 ++++++ .../CloudPulse/Utils/FilterBuilder.ts | 101 ++++++- .../CloudPulse/Widget/CloudPulseWidget.tsx | 29 +- .../shared/CloudPulseComponentRenderer.tsx | 6 +- .../shared/CloudPulseCustomSelect.test.tsx | 152 +++++++++++ .../shared/CloudPulseCustomSelect.tsx | 249 ++++++++++++++++++ .../shared/CloudPulseCustomSelectUtils.ts | 179 +++++++++++++ .../CloudPulseDashboardFilterBuilder.tsx | 26 +- packages/manager/src/mocks/serverHandlers.ts | 59 +---- .../src/queries/cloudpulse/customfilters.ts | 6 +- 12 files changed, 842 insertions(+), 70 deletions(-) create mode 100644 packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.test.tsx create mode 100644 packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx create mode 100644 packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx index c1758bc1d13..bb2f9ad310f 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx @@ -21,7 +21,10 @@ import { getIntervalIndex, } from '../Widget/components/CloudPulseIntervalSelect'; -import type { CloudPulseWidgetProperties } from '../Widget/CloudPulseWidget'; +import type { + CloudPulseMetricsAdditionalFilters, + CloudPulseWidgetProperties, +} from '../Widget/CloudPulseWidget'; import type { AvailableMetrics, Dashboard, @@ -31,6 +34,11 @@ import type { } from '@linode/api-v4'; export interface DashboardProperties { + /** + * Apart from above explicit filters, any additional filters for metrics endpoint will go here + */ + additionalFilters?: CloudPulseMetricsAdditionalFilters[]; + /** * Id of the selected dashboard */ @@ -69,6 +77,7 @@ export const CloudPulseDashboard = (props: DashboardProperties) => { manualRefreshTimeStamp, resources, savePref, + additionalFilters, } = props; const getJweTokenPayload = (): JWETokenPayLoad => { @@ -81,6 +90,7 @@ export const CloudPulseDashboard = (props: DashboardProperties) => { widget: Widgets ): CloudPulseWidgetProperties => { const graphProp: CloudPulseWidgetProperties = { + additionalFilters, ariaLabel: widget.label, authToken: '', availableMetrics: undefined, @@ -238,4 +248,4 @@ export const CloudPulseDashboard = (props: DashboardProperties) => { }; return ; -}; +}; \ No newline at end of file diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx index 9cfd7c3494f..7addf6c414e 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx @@ -7,7 +7,10 @@ import { StyledPlaceholder } from 'src/features/StackScripts/StackScriptBase/Sta import { GlobalFilters } from '../Overview/GlobalFilters'; import { REGION, RESOURCE_ID } from '../Utils/constants'; -import { checkIfAllMandatoryFiltersAreSelected } from '../Utils/FilterBuilder'; +import { + checkIfAllMandatoryFiltersAreSelected, + getFiltersForMetricsCallFromCustomSelect, +} from '../Utils/FilterBuilder'; import { FILTER_CONFIG } from '../Utils/FilterConfig'; import { useLoadUserPreferences } from '../Utils/UserPreference'; import { CloudPulseDashboard } from './CloudPulseDashboard'; @@ -97,6 +100,10 @@ export const CloudPulseDashboardLanding = () => { return ( { ); -}; +}; \ No newline at end of file diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts index 6460e985694..2d00576ecb6 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts @@ -4,16 +4,23 @@ import { buildXFilter, checkIfAllMandatoryFiltersAreSelected, checkIfWeNeedToDisableFilterByFilterKey, + constructAdditionalRequestFilters, + getCustomSelectProperties, + getFiltersForMetricsCallFromCustomSelect, getRegionProperties, getResourcesProperties, getTimeDurationProperties, } from './FilterBuilder'; import { FILTER_CONFIG } from './FilterConfig'; +import { CloudPulseSelectTypes } from './models'; +import { databaseQueries } from 'src/queries/databases/databases'; const mockDashboard = dashboardFactory.build(); const linodeConfig = FILTER_CONFIG.get('linode'); +const dbaasConfig = FILTER_CONFIG.get('dbaas'); + it('test getRegionProperties method', () => { const regionConfig = linodeConfig?.filters.find( (filterObj) => filterObj.name === 'Region' @@ -183,3 +190,76 @@ it('test checkIfAllMandatoryFiltersAreSelected method', () => { expect(result).toEqual(false); }); + +it('test getCustomSelectProperties method', () => { + let customSelectEngineConfig = dbaasConfig?.filters.find( + (filterObj) => filterObj.name === 'DB Engine' + ); + + expect(customSelectEngineConfig).toBeDefined(); + + if(customSelectEngineConfig) { + let result = getCustomSelectProperties({ + config : customSelectEngineConfig, + dashboard: mockDashboard, + isServiceAnalyticsIntegration: true, + }, vi.fn()); + + expect(result.options).toBeDefined(); + expect(result.options?.length).toEqual(2); + expect(result.savePreferences).toEqual(false); + expect(result.isMultiSelect).toEqual(false); + expect(result.disabled).toEqual(false); + + customSelectEngineConfig.configuration.type = CloudPulseSelectTypes.dynamic; + customSelectEngineConfig.configuration.apiV4QueryKey = databaseQueries.engines; + customSelectEngineConfig.configuration.isMultiSelect = true; + customSelectEngineConfig.configuration.options = undefined; + + result = getCustomSelectProperties({ + config : customSelectEngineConfig, + dashboard: mockDashboard, + isServiceAnalyticsIntegration: true, + }, vi.fn()); + + expect(result.apiV4QueryKey).toEqual(databaseQueries.engines); + expect(result.type).toEqual(CloudPulseSelectTypes.dynamic); + expect(result.savePreferences).toEqual(false); + expect(result.isMultiSelect).toEqual(true); + } +}); + +it('test getFiltersForMetricsCallFromCustomSelect method', () => { + let result = getFiltersForMetricsCallFromCustomSelect({ + 'resource_id' : [1,2,3] + }, 'linode'); + + expect(result).toBeDefined(); + expect(result.length).toEqual(1); + expect(result[0].filterKey).toEqual('resource_id'); + expect(result[0].filterValue).toEqual([1,2,3]); + + // test empty case + result = getFiltersForMetricsCallFromCustomSelect({ + 'resource_test_id': [1,2,3] + }, 'linode'); + expect(result).toBeDefined(); + expect(result.length).toEqual(0); +}); + +it('test constructAdditionalRequestFilters method', () => { + let result = constructAdditionalRequestFilters(getFiltersForMetricsCallFromCustomSelect({ + 'resource_id' : [1,2,3] + }, 'linode')) + + expect(result).toBeDefined(); + expect(result.length).toEqual(1); + expect(result[0].key).toEqual('resource_id'); + expect(result[0].operator).toEqual('in') + expect(result[0].value).toEqual("1,2,3"); + + result = constructAdditionalRequestFilters([]); + + expect(result).toBeDefined(); + expect(result.length).toEqual(0); +}); diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts index 90a1ea45f07..2feefdcecdd 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts @@ -1,15 +1,17 @@ import { RELATIVE_TIME_DURATION } from './constants'; import { FILTER_CONFIG } from './FilterConfig'; +import { CloudPulseSelectTypes, type CloudPulseServiceTypeFilters } from './models'; import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding'; +import type { CloudPulseCustomSelectProps } from '../shared/CloudPulseCustomSelect'; import type { CloudPulseRegionSelectProps } from '../shared/CloudPulseRegionSelect'; import type { CloudPulseResources, CloudPulseResourcesSelectProps, } from '../shared/CloudPulseResourcesSelect'; import type { CloudPulseTimeRangeSelectProps } from '../shared/CloudPulseTimeRangeSelect'; -import type { CloudPulseServiceTypeFilters } from './models'; -import type { Dashboard, Filter, TimeDuration } from '@linode/api-v4'; +import type { Dashboard, Filter, Filters, TimeDuration } from '@linode/api-v4'; +import { CloudPulseMetricsAdditionalFilters } from '../Widget/CloudPulseWidget'; interface CloudPulseFilterProperties { config: CloudPulseServiceTypeFilters; @@ -86,6 +88,51 @@ export const getResourcesProperties = ( }; }; +/** + * This function returns a CloudPulseCustomSelectProps based on the filter config and selected filters + * @param props - The cloudpulse filter properties selected so far + * @param handleCustomSelectChange - The call back function when a filter change happens + * @returns {CloudPulseCustomSelectProps} - Returns a property compatible for CloudPulseCustomSelect Component + */ +export const getCustomSelectProperties = ( + props: CloudPulseFilterProperties, + handleCustomSelectChange: (filterKey: string, value: FilterValueType) => void +): CloudPulseCustomSelectProps => { + const { + apiIdField, + apiLabelField, + apiV4QueryKey, + filterKey, + filterType, + isMultiSelect, + maxSelections, + options, + placeholder, + } = props.config.configuration; + const { dashboard, dependentFilters, isServiceAnalyticsIntegration } = props; + return { + apiResponseIdField: apiIdField, + apiResponseLabelField: apiLabelField, + apiV4QueryKey, + disabled: checkIfWeNeedToDisableFilterByFilterKey( + filterKey, + dependentFilters ?? {}, + dashboard + ), + filterKey, + filterType, + handleSelectionChange: handleCustomSelectChange, + isMultiSelect, + maxSelections, + options, + placeholder, + savePreferences: !isServiceAnalyticsIntegration, + type: options + ? CloudPulseSelectTypes.static + : CloudPulseSelectTypes.dynamic, + }; +}; + /** * This function helps in building the properties needed for time duration filter * @@ -206,3 +253,53 @@ export const checkIfAllMandatoryFiltersAreSelected = ( return value !== undefined && (!Array.isArray(value) || value.length > 0); }); }; + +/** + * @param selectedFilters The selected filters from the global filters view from custom select component + * @param serviceType The serviceType assosicated with the dashboard like linode, dbaas etc., + * @returns Constructs and returns the metrics call filters based on selected filters and service type + */ +export const getFiltersForMetricsCallFromCustomSelect = ( + selectedFilters: { + [key: string]: FilterValueType; + }, + serviceType: string +): CloudPulseMetricsAdditionalFilters[] => { + const serviceTypeConfig = FILTER_CONFIG.get(serviceType); + + // If configuration exists, filter and map it to the desired CloudPulseMetricsAdditionalFilters format + return serviceTypeConfig + ? serviceTypeConfig.filters + .filter(({ configuration }) => + configuration.isFilterable && + selectedFilters.hasOwnProperty(configuration.filterKey) + ) + .map(({ configuration }) => ({ + filterKey: configuration.filterKey, + filterValue: selectedFilters[configuration.filterKey], + })) + : []; +}; + +/** + * @param additionalFilters The additional filters selected from custom select components + * @returns The list of filters for the metric API call, based the additional custom select components + */ +export const constructAdditionalRequestFilters = ( + additionalFilters: CloudPulseMetricsAdditionalFilters[] +): Filters[] => { + const filters: Filters[] = []; + for (const filter of additionalFilters) { + if (filter) { + // push to the filters + filters.push({ + key: filter.filterKey, + operator: Array.isArray(filter.filterValue) ? 'in' : 'eq', + value: Array.isArray(filter.filterValue) + ? Array.of(filter.filterValue).join(',') + : String(filter.filterValue), + }); + } + } + return filters; +}; diff --git a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx index 906b537651a..e34f35ff04e 100644 --- a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx @@ -23,6 +23,7 @@ import { CloudPulseIntervalSelect } from './components/CloudPulseIntervalSelect' import { CloudPulseLineGraph } from './components/CloudPulseLineGraph'; import { ZoomIcon } from './components/Zoomer'; +import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding'; import type { CloudPulseResources } from '../shared/CloudPulseResourcesSelect'; import type { AvailableMetrics, @@ -32,8 +33,14 @@ import type { import type { Widgets } from '@linode/api-v4'; import type { DataSet } from 'src/components/LineGraph/LineGraph'; import type { Metrics } from 'src/utilities/statMetrics'; +import { constructAdditionalRequestFilters } from '../Utils/FilterBuilder'; export interface CloudPulseWidgetProperties { + /** + * Apart from above explicit filters, any additional filters for metrics endpoint will go here + */ + additionalFilters?: CloudPulseMetricsAdditionalFilters[]; + /** * Aria label for this widget */ @@ -100,6 +107,11 @@ export interface CloudPulseWidgetProperties { widget: Widgets; } +export interface CloudPulseMetricsAdditionalFilters { + filterKey: string; + filterValue: FilterValueType; +} + export interface LegendRow { data: Metrics; format: (value: number) => {}; @@ -228,12 +240,15 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { status, } = useCloudPulseMetricsQuery( serviceType, - getCloudPulseMetricRequest({ - duration, - resourceIds, - resources, - widget, - }), + { + ...getCloudPulseMetricRequest({ + duration, + resourceIds, + resources, + widget, + }), + filters: constructAdditionalRequestFilters(props.additionalFilters ?? []), // any additional dimension filters will be constructed and passed here + }, { authToken, isFlags: Boolean(flags), @@ -344,4 +359,4 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { ); -}; +}; \ No newline at end of file diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.tsx index c30a360d2e1..f4d5a7f2e27 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.tsx @@ -2,6 +2,7 @@ import React from 'react'; import NullComponent from 'src/components/NullComponent'; +import { CloudPulseCustomSelect, type CloudPulseCustomSelectProps } from './CloudPulseCustomSelect'; import { CloudPulseRegionSelect } from './CloudPulseRegionSelect'; import { CloudPulseResourcesSelect } from './CloudPulseResourcesSelect'; import { CloudPulseTimeRangeSelect } from './CloudPulseTimeRangeSelect'; @@ -14,6 +15,7 @@ import type { MemoExoticComponent } from 'react'; export interface CloudPulseComponentRendererProps { componentKey: string; componentProps: + | CloudPulseCustomSelectProps | CloudPulseRegionSelectProps | CloudPulseResourcesSelectProps | CloudPulseTimeRangeSelectProps; @@ -23,12 +25,14 @@ export interface CloudPulseComponentRendererProps { const Components: { [key: string]: MemoExoticComponent< React.ComponentType< + | CloudPulseCustomSelectProps | CloudPulseRegionSelectProps | CloudPulseResourcesSelectProps | CloudPulseTimeRangeSelectProps > >; } = { + customSelect: CloudPulseCustomSelect, region: CloudPulseRegionSelect, relative_time_duration: CloudPulseTimeRangeSelect, resource_id: CloudPulseResourcesSelect, @@ -44,4 +48,4 @@ const buildComponent = (props: CloudPulseComponentRendererProps) => { return ; }; -export default buildComponent; +export default buildComponent; \ No newline at end of file diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.test.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.test.tsx new file mode 100644 index 00000000000..5b3f2e7384f --- /dev/null +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.test.tsx @@ -0,0 +1,152 @@ +import { fireEvent } from '@testing-library/react'; +import React from 'react'; + +import { databaseQueries } from 'src/queries/databases/databases'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { CloudPulseSelectTypes } from '../Utils/models'; +import { CloudPulseCustomSelect } from './CloudPulseCustomSelect'; + +import type { CloudPulseServiceTypeFiltersOptions } from '../Utils/models'; + +const mockOptions: CloudPulseServiceTypeFiltersOptions[] = [ + { + id: '1', + label: 'Test1', + }, + { + id: '2', + label: 'Test2', + }, +]; + +const queryMocks = vi.hoisted(() => ({ + useGetCustomFiltersQuery: vi.fn().mockReturnValue({ + data: [ + { + id: '1', + label: 'Test1', + }, + { + id: '2', + label: 'Test2', + }, + ], + isError: false, + isLoading: false, + status: 'success', + }), +})); + +vi.mock('src/queries/cloudpulse/customfilters', async () => { + const actual = await vi.importActual('src/queries/cloudpulse/customfilters'); + return { + ...actual, + useGetCustomFiltersQuery: queryMocks.useGetCustomFiltersQuery, + }; +}); + +const testfilter = 'Select a Test Filter'; + +describe('CloudPulseCustomSelect component tests', () => { + it('should render a component successfully with required props static', () => { + const screen = renderWithTheme( + + ); + + expect(screen.getByPlaceholderText('Select a Test Filter')).toBeDefined(); + const keyDown = screen.getByTestId('KeyboardArrowDownIcon'); + fireEvent.click(keyDown); + fireEvent.click(screen.getByText('Test1')); + const textField = screen.getByTestId('textfield-input'); + expect(textField.getAttribute('value')).toEqual('Test1'); + }); + + it('should render a component successfully with required props static with multi select', () => { + const screen = renderWithTheme( + + ); + + expect(screen.getByPlaceholderText(testfilter)).toBeDefined(); + const keyDown = screen.getByTestId('KeyboardArrowDownIcon'); + fireEvent.click(keyDown); + expect(screen.getAllByText('Test1').length).toEqual(2); // here it should be 2 + expect(screen.getAllByText('Test2').length).toEqual(1); // since we didn't select this option it should be 1 + fireEvent.click(screen.getByText('Test2')); + + expect(screen.getAllByText('Test1').length).toEqual(2); // here it should be 2 + expect(screen.getAllByText('Test2').length).toEqual(2); // since we did select this option it should be 2 + + fireEvent.click(keyDown); // close the drop down + + expect(screen.getAllByText('Test1').length).toEqual(1); + expect(screen.getAllByText('Test2').length).toEqual(1); + }); + + it('should render a component successfully with required props dynamic', () => { + const selectionChnage = vi.fn(); + const screen = renderWithTheme( + + ); + expect(screen.getByPlaceholderText(testfilter)).toBeDefined(); + const keyDown = screen.getByTestId('KeyboardArrowDownIcon'); + fireEvent.click(keyDown); + fireEvent.click(screen.getByText('Test1')); + const textField = screen.getByTestId('textfield-input'); + expect(textField.getAttribute('value')).toEqual('Test1'); + expect(selectionChnage).toHaveBeenCalledTimes(1); + }); + + it('should render a component successfully with required props dynamic multi select', () => { + const selectionChnage = vi.fn(); + const screen = renderWithTheme( + + ); + expect(screen.getByPlaceholderText(testfilter)).toBeDefined(); + const keyDown = screen.getByTestId('KeyboardArrowDownIcon'); + fireEvent.click(keyDown); + fireEvent.click(screen.getByText('Test1')); + expect(screen.getAllByText('Test1').length).toEqual(2); // here it should be 2 + expect(screen.getAllByText('Test2').length).toEqual(1); // since we didn't select this option it should be 1 + fireEvent.click(screen.getByText('Test2')); + + expect(screen.getAllByText('Test1').length).toEqual(2); // here it should be 2 + expect(screen.getAllByText('Test2').length).toEqual(2); // since we did select this option it should be 2 + + fireEvent.click(keyDown); // close the drop down + + expect(screen.getAllByText('Test1').length).toEqual(1); + expect(screen.getAllByText('Test2').length).toEqual(1); + expect(selectionChnage).toHaveBeenCalledTimes(2); // check if selection change is called twice as we selected two options + }); +}); diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx new file mode 100644 index 00000000000..2953a1a2ca7 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx @@ -0,0 +1,249 @@ +import deepEqual from 'fast-deep-equal'; +import React from 'react'; + +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; +import { useGetCustomFiltersQuery } from 'src/queries/cloudpulse/customfilters'; + +import { + callSelectionChangeAndUpdateGlobalFilters, + getDefaultSelectionsFromPreferencesAndPublishSelectionChanges, +} from './CloudPulseCustomSelectUtils'; + +import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding'; +import type { + CloudPulseServiceTypeFiltersOptions, + QueryFunctionAndKey, +} from '../Utils/models'; + +/** + * This is the properties requires for CloudPulseCustomSelect Components + * + */ +export interface CloudPulseCustomSelectProps { + /** + * The id field of the response returned from the API + */ + apiResponseIdField?: string; + + /** + * The label field of the response returned from the API + */ + apiResponseLabelField?: string; + + /** + * The api URL which contains the list of filters, passed when the select type is dynamic + */ + apiV4QueryKey?: QueryFunctionAndKey; + + /** + * The selections to be cleared on some filter updates + */ + clearSelections?: string[]; + + /** + * This property says, whether or not to disable the selection component + */ + disabled?: boolean; + + /** + * The errorText that needs to be displayed + */ + errorText?: string; + + /** + * The filterKey that needs to be used + */ + filterKey: string; + + /** + * The type of the filter like string, number etc., + */ + filterType: string; + + /** + * The callback function , that will be called on a filter change + * @param filterKey - The filterKey of the component + * @param value - The selected filter value + */ + handleSelectionChange: (filterKey: string, value: FilterValueType) => void; + /** + * If true, multiselect is allowed, otherwise false + */ + isMultiSelect?: boolean; + + /** + * The maximum selections that the user can make incase of multiselect + */ + maxSelections?: number; + + /** + * The options to be listed down in the autocomplete if the select type is static + */ + options?: CloudPulseServiceTypeFiltersOptions[]; + + /** + * The placeholder that needs to displayed + */ + placeholder?: string; + + /** + * This property controls whether to save the preferences or not + */ + savePreferences?: boolean; + + /** + * The cloud pulse select types, it can be static or dynamic depending on the use case + */ + type: CloudPulseSelectTypes; +} + +export enum CloudPulseSelectTypes { + dynamic, + static, +} + +export const CloudPulseCustomSelect = React.memo( + (props: CloudPulseCustomSelectProps) => { + const { + apiResponseIdField, + apiResponseLabelField, + apiV4QueryKey, + clearSelections, + disabled, + filterKey, + handleSelectionChange, + isMultiSelect, + maxSelections, + options, + placeholder, + savePreferences, + type, + } = props; + + const [selectedResource, setResource] = React.useState< + | CloudPulseServiceTypeFiltersOptions + | CloudPulseServiceTypeFiltersOptions[] + | undefined + >(); + + const { + data: queriedResources, + isError, + isLoading, + } = useGetCustomFiltersQuery({ + apiV4QueryKey, + enabled: + apiV4QueryKey !== undefined && + (disabled !== undefined ? !disabled : true), + filter: {}, + idField: apiResponseIdField ? apiResponseIdField : 'id', + labelField: apiResponseLabelField ? apiResponseLabelField : 'label', + }); + + let staticErrorText = ''; + + React.useEffect(() => { + if (!selectedResource) { + setResource( + getDefaultSelectionsFromPreferencesAndPublishSelectionChanges( + { + filterKey, + handleSelectionChange, + isMultiSelect: isMultiSelect ?? false, + options: options ?? [], + savePreferences: savePreferences ?? false, + } + ) + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [savePreferences, options, apiV4QueryKey]); // only execute this use efffect one time or if savePreferences or options or dataApiUrl changes + + const handleChange = ( + _: React.SyntheticEvent, + value: + | CloudPulseServiceTypeFiltersOptions + | CloudPulseServiceTypeFiltersOptions[] + | null + ) => { + callSelectionChangeAndUpdateGlobalFilters({ + clearSelections: clearSelections ?? [], + filterKey, + handleSelectionChange, + maxSelections, + value, + }); + setResource(Array.isArray(value) ? [...value] : value ?? undefined); + }; + + // check for input prop errors + if ( + CloudPulseSelectTypes.static === type && + (!options || options.length === 0) + ) { + staticErrorText = 'Pass predefined options for static select type'; + } + + if (CloudPulseSelectTypes.dynamic === type && !apiV4QueryKey) { + staticErrorText = 'Pass API Url for dynamic select type'; + } + + return ( + 0 + } + errorText={ + staticErrorText.length > 0 + ? staticErrorText + : isError + ? 'Error while loading from API' + : '' + } + options={ + type === CloudPulseSelectTypes.static + ? options || [] + : queriedResources || [] + } + textFieldProps={{ + hideLabel: true, + }} + getOptionLabel={(option) => option.label ?? ''} + isOptionEqualToValue={(option, value) => option.label === value.label} + label="Select a Value" + multiple={isMultiSelect} + onChange={handleChange} + placeholder={placeholder ? placeholder : 'Select a Value'} + value={selectedResource ? selectedResource : isMultiSelect ? [] : null} + /> + ); + }, + compareProps +); + +function compareProps( + prevProps: CloudPulseCustomSelectProps, + nextProps: CloudPulseCustomSelectProps +): boolean { + // these properties can be extended going forward + const keysToCompare: (keyof CloudPulseCustomSelectProps)[] = [ + 'apiV4QueryKey', + 'disabled', + ]; + + for (const key of keysToCompare) { + if (prevProps[key] !== nextProps[key]) { + return false; + } + } + + // Deep comparison for options + if (!deepEqual(prevProps.options, nextProps.options)) { + return false; + } + + // Ignore function props in comparison + return true; +} \ No newline at end of file diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts new file mode 100644 index 00000000000..b800796828a --- /dev/null +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts @@ -0,0 +1,179 @@ +import { + getUserPreferenceObject, + updateGlobalFilterPreference, +} from '../Utils/UserPreference'; + +import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding'; +import type { CloudPulseServiceTypeFiltersOptions } from '../Utils/models'; + +/** + * The interface for selecting the default value from the user preferences + */ +interface CloudPulseCustomSelectDefaultValueProps { + /** + * The filter Key of the current rendered custom select component + */ + filterKey: string; + /** + * The callback for the selection changes happening in the custom select component + */ + handleSelectionChange: (filterKey: string, value: FilterValueType) => void; + + /** + * This indicates whether we need multiselect for the component or not + */ + isMultiSelect: boolean; + + /** + * The current listed options in the custom select component + */ + options: CloudPulseServiceTypeFiltersOptions[]; + + /** + * This indicates whether we need to save preferences or not + */ + savePreferences: boolean; +} + +/** + * The interface of publishing the selection change and updating the user preferences accordingly + */ +interface CloudPulseCustomSelectionChangeProps { + /** + * The list of filters needs to be cleared on selections + */ + clearSelections: string[]; + /** + * The current filter key of the rendered custom select component + */ + filterKey: string; + /** + * The callback for the selection changes happening in the custom select component + */ + handleSelectionChange: (filterKey: string, value: FilterValueType) => void; + + /** + * The maximum number of selections that needs to be allowed + */ + maxSelections?: number; + + /** + * The listed options in the custom select component + */ + value: + | CloudPulseServiceTypeFiltersOptions + | CloudPulseServiceTypeFiltersOptions[] + | null; +} + +/** + * This function returns the default selections based on the user preference and options listed + * @param defaultSelectionProps - The props needed for getting the default selections + * @returns + */ +export const getDefaultSelectionsFromPreferencesAndPublishSelectionChanges = ( + defaultSelectionProps: CloudPulseCustomSelectDefaultValueProps +): + | CloudPulseServiceTypeFiltersOptions + | CloudPulseServiceTypeFiltersOptions[] + | undefined => { + const { + filterKey, + handleSelectionChange, + isMultiSelect, + options, + savePreferences, + } = defaultSelectionProps; + + const defaultValue = savePreferences + ? getUserPreferenceObject()[filterKey] + : undefined; + if (!options || options.length === 0) { + return isMultiSelect ? [] : undefined; + } + + // Handle the case when there is no default value and preferences are not saved + if (!defaultValue && !savePreferences) { + const initialSelection = isMultiSelect ? [options[0]] : options[0]; + handleSelectionChange( + filterKey, + isMultiSelect ? [options[0].id] : options[0].id + ); + return initialSelection; + } + + if (isMultiSelect) { + // Handle multiple selections + const selectedValues = options.filter(({ id }) => + (Array.isArray(defaultValue) ? defaultValue : [defaultValue]).includes( + String(id) + ) + ); + handleSelectionChange( + filterKey, + selectedValues.map(({ id }) => id) + ); + return selectedValues; + } + + // Handle single selection + const selectedValue = options.find(({ id }) => id === defaultValue); + if (selectedValue) { + handleSelectionChange(filterKey, selectedValue.id); + } + return selectedValue; +}; + +/** + * This functions calls the selection change callback and updates the latest selected filter in the preferences + * @param selectionChangeProps - The props needed for selecting the new filter and updating the global preferences + */ + +export const callSelectionChangeAndUpdateGlobalFilters = ( + selectionChangeProps: CloudPulseCustomSelectionChangeProps +) => { + const { + clearSelections, + filterKey, + handleSelectionChange, + maxSelections, + } = selectionChangeProps; + + let { value } = selectionChangeProps; + if (Array.isArray(value)) { + // in multi select case, it will be an array, apply maxselections if provided + if (maxSelections && value.length > maxSelections) { + value = value.slice(0, maxSelections); + } + + // pubish the selection change + handleSelectionChange( + filterKey, + value.map(({ id }) => id.toString()) + ); + + // update the preferences + updateGlobalFilterPreference({ + [filterKey]: value.map(({ id }) => id.toString()), + }); + + // update the clear selections in the preference + if (clearSelections) { + clearSelections.forEach((selection) => + updateGlobalFilterPreference({ [selection]: undefined }) + ); + } + } else { + // in case of non multi select, just publish the changes and update the preferences + handleSelectionChange(filterKey, value ? value.id.toString() : undefined); + updateGlobalFilterPreference({ + [filterKey]: value ? value.id.toString() : null, + }); + } + + if (!value) { + updateGlobalFilterPreference({ + [filterKey]: null, + }); + } +}; diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx index 43a29430142..c5b4bc4d0c5 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx @@ -16,6 +16,7 @@ import { RESOURCE_ID, } from '../Utils/constants'; import { + getCustomSelectProperties, getRegionProperties, getResourcesProperties, } from '../Utils/FilterBuilder'; @@ -111,6 +112,13 @@ export const CloudPulseDashboardFilterBuilder = React.memo( [emitFilterChangeByFilterKey] ); + const handleCustomSelectChange = React.useCallback( + (filterKey: string, value: FilterValueType) => { + emitFilterChangeByFilterKey(filterKey, value); + }, + [emitFilterChangeByFilterKey] + ); + const getProps = React.useCallback( (config: CloudPulseServiceTypeFilters) => { if (config.configuration.filterKey === REGION) { @@ -129,13 +137,22 @@ export const CloudPulseDashboardFilterBuilder = React.memo( handleResourceChange ); } else { - return {}; + return getCustomSelectProperties( + { + config, + dashboard, + dependentFilters: dependentFilterReference.current, + isServiceAnalyticsIntegration, + }, + handleCustomSelectChange + ); } }, [ dashboard, handleRegionChange, handleResourceChange, + handleCustomSelectChange, isServiceAnalyticsIntegration, ] ); @@ -168,7 +185,10 @@ export const CloudPulseDashboardFilterBuilder = React.memo( .map((filter, index) => ( {RenderComponent({ - componentKey: filter.configuration.filterKey, + componentKey: + filter.configuration.type !== undefined + ? 'customSelect' + : filter.configuration.filterKey, componentProps: { ...getProps(filter) }, key: index + filter.configuration.filterKey, })} @@ -234,4 +254,4 @@ function compareProps( newProps: CloudPulseDashboardFilterBuilderProps ) { return oldProps.dashboard?.id === newProps.dashboard?.id; -} +} \ No newline at end of file diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 8a1a0572734..f46e627b28a 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -16,6 +16,7 @@ import { contactFactory, credentialFactory, creditPaymentResponseFactory, + dashboardFactory, databaseBackupFactory, databaseEngineFactory, databaseFactory, @@ -2270,56 +2271,14 @@ export const handlers = [ http.get('*/v4/monitor/services/linode/dashboards', () => { const response = { data: [ - { - 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', - }, - ], - }, + dashboardFactory.build({ + label:'Linode Dashboard', + service_type: 'linode' + }), + dashboardFactory.build({ + label: 'Dbaas Dashboard', + service_type: 'dbaas' + }) ], }; diff --git a/packages/manager/src/queries/cloudpulse/customfilters.ts b/packages/manager/src/queries/cloudpulse/customfilters.ts index ad60921b5c6..17a94bdfb00 100644 --- a/packages/manager/src/queries/cloudpulse/customfilters.ts +++ b/packages/manager/src/queries/cloudpulse/customfilters.ts @@ -15,7 +15,7 @@ interface CustomFilterQueryProps { /** * The Built in API-V4 query factory functions like databaseQueries.types, databaseQueries.engines etc., makes use of existing query key and optimises cache */ - apiV4QueryKey: QueryFunctionAndKey; + apiV4QueryKey?: QueryFunctionAndKey; /** * This indicates whether or not to enable the query */ @@ -53,7 +53,7 @@ export const useGetCustomFiltersQuery = ( CloudPulseServiceTypeFiltersOptions[] >({ // receive filters and return only id and label - enabled, + enabled: enabled && apiV4QueryKey !== undefined, ...apiV4QueryKey, select: ( filters: QueryFunctionType @@ -90,4 +90,4 @@ const getStringValue = ( } } return undefined; -}; +}; \ No newline at end of file From f73f2a6439c2bd342f53521979c65f4f0e26fad3 Mon Sep 17 00:00:00 2001 From: vmangalr Date: Wed, 21 Aug 2024 13:14:39 +0530 Subject: [PATCH 02/20] upcoming: [DI-20348] - Added changeset --- .../.changeset/pr-10807-upcoming-features-1724226251911.md | 5 +++++ .../manager/src/features/CloudPulse/Utils/FilterBuilder.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 packages/manager/.changeset/pr-10807-upcoming-features-1724226251911.md diff --git a/packages/manager/.changeset/pr-10807-upcoming-features-1724226251911.md b/packages/manager/.changeset/pr-10807-upcoming-features-1724226251911.md new file mode 100644 index 00000000000..c8188d70991 --- /dev/null +++ b/packages/manager/.changeset/pr-10807-upcoming-features-1724226251911.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add new CloudPulseCustomSelect component and integrate with the global filter builder. ([#10807](https://github.com/linode/manager/pull/10807)) diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts index 2feefdcecdd..b9b1ca7e829 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts @@ -271,7 +271,7 @@ export const getFiltersForMetricsCallFromCustomSelect = ( return serviceTypeConfig ? serviceTypeConfig.filters .filter(({ configuration }) => - configuration.isFilterable && + configuration.isFilterable && !configuration.isMetricsFilter && selectedFilters.hasOwnProperty(configuration.filterKey) ) .map(({ configuration }) => ({ From 145983a80e03071cdb7e0d18eb23b27f32d00f4f Mon Sep 17 00:00:00 2001 From: vmangalr Date: Wed, 21 Aug 2024 14:25:58 +0530 Subject: [PATCH 03/20] upcoming: [DI-20348] - Code refactoring changes --- .../CloudPulse/Widget/CloudPulseWidget.tsx | 3 ++- .../shared/CloudPulseCustomSelect.test.tsx | 27 ++++++++++--------- .../shared/CloudPulseCustomSelect.tsx | 9 ++++--- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx index e34f35ff04e..09b6ecabb20 100644 --- a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx @@ -137,6 +137,7 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { serviceType, timeStamp, unit, + additionalFilters, } = props; const flags = useFlags(); @@ -247,7 +248,7 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { resources, widget, }), - filters: constructAdditionalRequestFilters(props.additionalFilters ?? []), // any additional dimension filters will be constructed and passed here + filters: constructAdditionalRequestFilters(additionalFilters ?? []), // any additional dimension filters will be constructed and passed here }, { authToken, diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.test.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.test.tsx index 5b3f2e7384f..4b7ecbd4c29 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.test.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.test.tsx @@ -46,7 +46,8 @@ vi.mock('src/queries/cloudpulse/customfilters', async () => { }; }); -const testfilter = 'Select a Test Filter'; +const testFilter = 'Select a Test Filter'; +const keyboardArrowDownIcon = 'KeyboardArrowDownIcon'; describe('CloudPulseCustomSelect component tests', () => { it('should render a component successfully with required props static', () => { @@ -56,13 +57,13 @@ describe('CloudPulseCustomSelect component tests', () => { filterType="number" handleSelectionChange={vi.fn()} options={mockOptions} - placeholder={testfilter} + placeholder={testFilter} type={CloudPulseSelectTypes.static} /> ); - expect(screen.getByPlaceholderText('Select a Test Filter')).toBeDefined(); - const keyDown = screen.getByTestId('KeyboardArrowDownIcon'); + expect(screen.getByPlaceholderText(testFilter)).toBeDefined(); + const keyDown = screen.getByTestId(keyboardArrowDownIcon); fireEvent.click(keyDown); fireEvent.click(screen.getByText('Test1')); const textField = screen.getByTestId('textfield-input'); @@ -77,13 +78,13 @@ describe('CloudPulseCustomSelect component tests', () => { handleSelectionChange={vi.fn()} isMultiSelect={true} options={[...mockOptions]} - placeholder={testfilter} + placeholder={testFilter} type={CloudPulseSelectTypes.static} /> ); - expect(screen.getByPlaceholderText(testfilter)).toBeDefined(); - const keyDown = screen.getByTestId('KeyboardArrowDownIcon'); + expect(screen.getByPlaceholderText(testFilter)).toBeDefined(); + const keyDown = screen.getByTestId(keyboardArrowDownIcon); fireEvent.click(keyDown); expect(screen.getAllByText('Test1').length).toEqual(2); // here it should be 2 expect(screen.getAllByText('Test2').length).toEqual(1); // since we didn't select this option it should be 1 @@ -106,12 +107,12 @@ describe('CloudPulseCustomSelect component tests', () => { filterKey="testfilter" filterType="number" handleSelectionChange={selectionChnage} - placeholder={testfilter} + placeholder={testFilter} type={CloudPulseSelectTypes.dynamic} /> ); - expect(screen.getByPlaceholderText(testfilter)).toBeDefined(); - const keyDown = screen.getByTestId('KeyboardArrowDownIcon'); + expect(screen.getByPlaceholderText(testFilter)).toBeDefined(); + const keyDown = screen.getByTestId(keyboardArrowDownIcon); fireEvent.click(keyDown); fireEvent.click(screen.getByText('Test1')); const textField = screen.getByTestId('textfield-input'); @@ -128,12 +129,12 @@ describe('CloudPulseCustomSelect component tests', () => { filterType="number" handleSelectionChange={selectionChnage} isMultiSelect={true} - placeholder={testfilter} + placeholder={testFilter} type={CloudPulseSelectTypes.dynamic} /> ); - expect(screen.getByPlaceholderText(testfilter)).toBeDefined(); - const keyDown = screen.getByTestId('KeyboardArrowDownIcon'); + expect(screen.getByPlaceholderText(testFilter)).toBeDefined(); + const keyDown = screen.getByTestId(keyboardArrowDownIcon); fireEvent.click(keyDown); fireEvent.click(screen.getByText('Test1')); expect(screen.getAllByText('Test1').length).toEqual(2); // here it should be 2 diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx index 2953a1a2ca7..7520e239080 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx @@ -140,8 +140,6 @@ export const CloudPulseCustomSelect = React.memo( labelField: apiResponseLabelField ? apiResponseLabelField : 'label', }); - let staticErrorText = ''; - React.useEffect(() => { if (!selectedResource) { setResource( @@ -176,6 +174,8 @@ export const CloudPulseCustomSelect = React.memo( setResource(Array.isArray(value) ? [...value] : value ?? undefined); }; + let staticErrorText = ''; + // check for input prop errors if ( CloudPulseSelectTypes.static === type && @@ -204,12 +204,13 @@ export const CloudPulseCustomSelect = React.memo( } options={ type === CloudPulseSelectTypes.static - ? options || [] - : queriedResources || [] + ? options ?? [] + : queriedResources ?? [] } textFieldProps={{ hideLabel: true, }} + getOptionLabel={(option) => option.label ?? ''} isOptionEqualToValue={(option, value) => option.label === value.label} label="Select a Value" From bed370ccfe32a8f53c94c4236fe099cdc792b8f8 Mon Sep 17 00:00:00 2001 From: vmangalr Date: Wed, 21 Aug 2024 16:15:59 +0530 Subject: [PATCH 04/20] upcoming: [DI-20348] - Code refactoring and method optimisations --- .../CloudPulse/Utils/FilterBuilder.test.ts | 22 +--- .../CloudPulse/Utils/FilterBuilder.ts | 42 ++++++-- .../shared/CloudPulseCustomSelect.tsx | 26 ++--- .../shared/CloudPulseCustomSelectUtils.ts | 102 +++++++++--------- .../src/queries/cloudpulse/customfilters.ts | 2 +- 5 files changed, 101 insertions(+), 93 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts index 2d00576ecb6..92ac6fbe273 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts @@ -14,6 +14,7 @@ import { import { FILTER_CONFIG } from './FilterConfig'; import { CloudPulseSelectTypes } from './models'; import { databaseQueries } from 'src/queries/databases/databases'; +import { RESOURCES } from './constants'; const mockDashboard = dashboardFactory.build(); @@ -201,7 +202,7 @@ it('test getCustomSelectProperties method', () => { if(customSelectEngineConfig) { let result = getCustomSelectProperties({ config : customSelectEngineConfig, - dashboard: mockDashboard, + dashboard: {...mockDashboard, service_type:'dbaas'}, isServiceAnalyticsIntegration: true, }, vi.fn()); @@ -210,6 +211,8 @@ it('test getCustomSelectProperties method', () => { expect(result.savePreferences).toEqual(false); expect(result.isMultiSelect).toEqual(false); expect(result.disabled).toEqual(false); + expect(result.clearDependentSelections).toBeDefined(); + expect(result.clearDependentSelections?.includes(RESOURCES)).toBe(true); customSelectEngineConfig.configuration.type = CloudPulseSelectTypes.dynamic; customSelectEngineConfig.configuration.apiV4QueryKey = databaseQueries.engines; @@ -234,15 +237,6 @@ it('test getFiltersForMetricsCallFromCustomSelect method', () => { 'resource_id' : [1,2,3] }, 'linode'); - expect(result).toBeDefined(); - expect(result.length).toEqual(1); - expect(result[0].filterKey).toEqual('resource_id'); - expect(result[0].filterValue).toEqual([1,2,3]); - - // test empty case - result = getFiltersForMetricsCallFromCustomSelect({ - 'resource_test_id': [1,2,3] - }, 'linode'); expect(result).toBeDefined(); expect(result.length).toEqual(0); }); @@ -252,14 +246,6 @@ it('test constructAdditionalRequestFilters method', () => { 'resource_id' : [1,2,3] }, 'linode')) - expect(result).toBeDefined(); - expect(result.length).toEqual(1); - expect(result[0].key).toEqual('resource_id'); - expect(result[0].operator).toEqual('in') - expect(result[0].value).toEqual("1,2,3"); - - result = constructAdditionalRequestFilters([]); - expect(result).toBeDefined(); expect(result.length).toEqual(0); }); diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts index b9b1ca7e829..d71e01e7d94 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts @@ -1,4 +1,4 @@ -import { RELATIVE_TIME_DURATION } from './constants'; +import { RELATIVE_TIME_DURATION, RESOURCE_ID, RESOURCES } from './constants'; import { FILTER_CONFIG } from './FilterConfig'; import { CloudPulseSelectTypes, type CloudPulseServiceTypeFilters } from './models'; @@ -89,10 +89,9 @@ export const getResourcesProperties = ( }; /** - * This function returns a CloudPulseCustomSelectProps based on the filter config and selected filters - * @param props - The cloudpulse filter properties selected so far - * @param handleCustomSelectChange - The call back function when a filter change happens - * @returns {CloudPulseCustomSelectProps} - Returns a property compatible for CloudPulseCustomSelect Component + * @param props The cloudpulse filter properties selected so far + * @param handleCustomSelectChange The call back function when a filter change happens + * @returns {CloudPulseCustomSelectProps} Returns a property compatible for CloudPulseCustomSelect Component */ export const getCustomSelectProperties = ( props: CloudPulseFilterProperties, @@ -130,6 +129,7 @@ export const getCustomSelectProperties = ( type: options ? CloudPulseSelectTypes.static : CloudPulseSelectTypes.dynamic, + clearDependentSelections: getDependantFiltersByFilterKey(filterKey, dashboard), }; }; @@ -268,17 +268,16 @@ export const getFiltersForMetricsCallFromCustomSelect = ( const serviceTypeConfig = FILTER_CONFIG.get(serviceType); // If configuration exists, filter and map it to the desired CloudPulseMetricsAdditionalFilters format - return serviceTypeConfig - ? serviceTypeConfig.filters + return serviceTypeConfig?.filters .filter(({ configuration }) => configuration.isFilterable && !configuration.isMetricsFilter && - selectedFilters.hasOwnProperty(configuration.filterKey) + selectedFilters[configuration.filterKey] ) .map(({ configuration }) => ({ filterKey: configuration.filterKey, filterValue: selectedFilters[configuration.filterKey], })) - : []; + ?? []; }; /** @@ -303,3 +302,28 @@ export const constructAdditionalRequestFilters = ( } return filters; }; + +/** + * + * @param filterKey The filterKey of the actual filter + * @param dashboard The selected dashboard from the global filter view + * @returns The filterKeys that needs to be removed from the preferences + */ +const getDependantFiltersByFilterKey = + (filterKey: string, dashboard: Dashboard): string[] => { + const serviceTypeConfig = FILTER_CONFIG.get(dashboard.service_type!); + + if (!serviceTypeConfig) { + return []; + } + + return serviceTypeConfig.filters + .filter((filter) => + filter?.configuration?.dependency?.includes(filterKey) + ) + .map(({configuration}) => + configuration.filterKey === RESOURCE_ID + ? RESOURCES + : configuration.filterKey + ); + }; diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx index 7520e239080..c765fdfaafc 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx @@ -36,9 +36,9 @@ export interface CloudPulseCustomSelectProps { apiV4QueryKey?: QueryFunctionAndKey; /** - * The selections to be cleared on some filter updates + * The dependent selections to be cleared on this filter update */ - clearSelections?: string[]; + clearDependentSelections?: string[]; /** * This property says, whether or not to disable the selection component @@ -108,7 +108,7 @@ export const CloudPulseCustomSelect = React.memo( apiResponseIdField, apiResponseLabelField, apiV4QueryKey, - clearSelections, + clearDependentSelections, disabled, filterKey, handleSelectionChange, @@ -164,14 +164,14 @@ export const CloudPulseCustomSelect = React.memo( | CloudPulseServiceTypeFiltersOptions[] | null ) => { - callSelectionChangeAndUpdateGlobalFilters({ - clearSelections: clearSelections ?? [], + const filterdValue = callSelectionChangeAndUpdateGlobalFilters({ + clearSelections: clearDependentSelections ?? [], filterKey, handleSelectionChange, maxSelections, value, }); - setResource(Array.isArray(value) ? [...value] : value ?? undefined); + setResource(Array.isArray(filterdValue) ? [...filterdValue] : filterdValue ?? undefined); }; let staticErrorText = ''; @@ -185,15 +185,17 @@ export const CloudPulseCustomSelect = React.memo( } if (CloudPulseSelectTypes.dynamic === type && !apiV4QueryKey) { - staticErrorText = 'Pass API Url for dynamic select type'; + staticErrorText = 'Pass API Factory for dynamic select type'; } + const isAutoCompleteDisabled = disabled || ((isLoading || isError) && type === CloudPulseSelectTypes.dynamic) || + (!queriedResources && !(options && options.length)) || + staticErrorText.length > 0; + return ( 0 + isAutoCompleteDisabled } errorText={ staticErrorText.length > 0 @@ -210,13 +212,11 @@ export const CloudPulseCustomSelect = React.memo( textFieldProps={{ hideLabel: true, }} - - getOptionLabel={(option) => option.label ?? ''} isOptionEqualToValue={(option, value) => option.label === value.label} label="Select a Value" multiple={isMultiSelect} onChange={handleChange} - placeholder={placeholder ? placeholder : 'Select a Value'} + placeholder={placeholder ?? 'Select a Value'} value={selectedResource ? selectedResource : isMultiSelect ? [] : null} /> ); diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts index b800796828a..541be5d5bed 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts @@ -102,26 +102,25 @@ export const getDefaultSelectionsFromPreferencesAndPublishSelectionChanges = ( return initialSelection; } - if (isMultiSelect) { - // Handle multiple selections - const selectedValues = options.filter(({ id }) => - (Array.isArray(defaultValue) ? defaultValue : [defaultValue]).includes( - String(id) - ) - ); - handleSelectionChange( - filterKey, - selectedValues.map(({ id }) => id) - ); - return selectedValues; - } + const selectedValues = options.filter(({ id }) => + (Array.isArray(defaultValue) ? defaultValue : [defaultValue]).includes( + String(id) + ) + ); - // Handle single selection - const selectedValue = options.find(({ id }) => id === defaultValue); - if (selectedValue) { - handleSelectionChange(filterKey, selectedValue.id); - } - return selectedValue; + handleSelectionChange( + filterKey, + selectedValues && selectedValues.length > 0 + ? isMultiSelect + ? selectedValues.map(({ id }) => id) + : selectedValues[0].id + : undefined // if this is multiselect, return list of ids, otherwise return single id + ); + return selectedValues && selectedValues.length > 0 + ? isMultiSelect + ? selectedValues + : selectedValues[0] + : undefined; }; /** @@ -131,7 +130,10 @@ export const getDefaultSelectionsFromPreferencesAndPublishSelectionChanges = ( export const callSelectionChangeAndUpdateGlobalFilters = ( selectionChangeProps: CloudPulseCustomSelectionChangeProps -) => { +): + | CloudPulseServiceTypeFiltersOptions + | CloudPulseServiceTypeFiltersOptions[] + | null => { const { clearSelections, filterKey, @@ -140,40 +142,36 @@ export const callSelectionChangeAndUpdateGlobalFilters = ( } = selectionChangeProps; let { value } = selectionChangeProps; - if (Array.isArray(value)) { - // in multi select case, it will be an array, apply maxselections if provided - if (maxSelections && value.length > maxSelections) { - value = value.slice(0, maxSelections); - } - // pubish the selection change - handleSelectionChange( - filterKey, - value.map(({ id }) => id.toString()) - ); - - // update the preferences - updateGlobalFilterPreference({ - [filterKey]: value.map(({ id }) => id.toString()), - }); - - // update the clear selections in the preference - if (clearSelections) { - clearSelections.forEach((selection) => - updateGlobalFilterPreference({ [selection]: undefined }) - ); - } - } else { - // in case of non multi select, just publish the changes and update the preferences - handleSelectionChange(filterKey, value ? value.id.toString() : undefined); - updateGlobalFilterPreference({ - [filterKey]: value ? value.id.toString() : null, - }); + if (Array.isArray(value) && maxSelections && value.length > maxSelections) { + value = value.slice(0, maxSelections); } - if (!value) { - updateGlobalFilterPreference({ - [filterKey]: null, - }); + // pubish the selection change + handleSelectionChange( + filterKey, + value + ? Array.isArray(value) + ? value.map(({ id }) => String(id)) // if array publish list of ids, else only id + : String(value.id) + : undefined + ); + + // update the preferences + updateGlobalFilterPreference({ + [filterKey]: value + ? Array.isArray(value) + ? value.map(({ id }) => String(id)) // if array publish list of ids, else only id + : String(value.id) + : undefined, + }); + + // update the clear selections in the preference + if (clearSelections) { + clearSelections.forEach((selection) => + updateGlobalFilterPreference({ [selection]: undefined }) + ); } + + return value; }; diff --git a/packages/manager/src/queries/cloudpulse/customfilters.ts b/packages/manager/src/queries/cloudpulse/customfilters.ts index 17a94bdfb00..5b0ba5035e0 100644 --- a/packages/manager/src/queries/cloudpulse/customfilters.ts +++ b/packages/manager/src/queries/cloudpulse/customfilters.ts @@ -54,7 +54,7 @@ export const useGetCustomFiltersQuery = ( >({ // receive filters and return only id and label enabled: enabled && apiV4QueryKey !== undefined, - ...apiV4QueryKey, + ...(apiV4QueryKey ?? {}), select: ( filters: QueryFunctionType ): CloudPulseServiceTypeFiltersOptions[] => { From 8507623a93a96949bf090ed678bd62d414d4a52f Mon Sep 17 00:00:00 2001 From: vmangalr Date: Wed, 21 Aug 2024 16:20:41 +0530 Subject: [PATCH 05/20] upcoming: [DI-20348] - Simplify code logic --- .../shared/CloudPulseCustomSelectUtils.ts | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts index 541be5d5bed..ac9b96318bb 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts @@ -147,23 +147,18 @@ export const callSelectionChangeAndUpdateGlobalFilters = ( value = value.slice(0, maxSelections); } + const result = value + ? Array.isArray(value) + ? value.map(({ id }) => String(id)) // if array publish list of ids, else only id + : String(value.id) + : undefined; + // pubish the selection change - handleSelectionChange( - filterKey, - value - ? Array.isArray(value) - ? value.map(({ id }) => String(id)) // if array publish list of ids, else only id - : String(value.id) - : undefined - ); + handleSelectionChange(filterKey, result); // update the preferences updateGlobalFilterPreference({ - [filterKey]: value - ? Array.isArray(value) - ? value.map(({ id }) => String(id)) // if array publish list of ids, else only id - : String(value.id) - : undefined, + [filterKey]: result, }); // update the clear selections in the preference From 34278aa72e8373bc7b2ff01098125e3b790bb7ea Mon Sep 17 00:00:00 2001 From: vmangalr Date: Wed, 21 Aug 2024 17:02:19 +0530 Subject: [PATCH 06/20] upcoming: [DI-20348] - More refactoring --- .../src/features/CloudPulse/Utils/FilterBuilder.ts | 2 +- .../CloudPulse/shared/CloudPulseCustomSelect.tsx | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts index d71e01e7d94..9c8c0033a84 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts @@ -311,7 +311,7 @@ export const constructAdditionalRequestFilters = ( */ const getDependantFiltersByFilterKey = (filterKey: string, dashboard: Dashboard): string[] => { - const serviceTypeConfig = FILTER_CONFIG.get(dashboard.service_type!); + const serviceTypeConfig = FILTER_CONFIG.get(dashboard.service_type); if (!serviceTypeConfig) { return []; diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx index c765fdfaafc..30c5c0e7500 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx @@ -192,17 +192,19 @@ export const CloudPulseCustomSelect = React.memo( (!queriedResources && !(options && options.length)) || staticErrorText.length > 0; + staticErrorText = staticErrorText.length > 0 + ? staticErrorText + : isError + ? 'Error while loading from API' + : ''; + return ( 0 - ? staticErrorText - : isError - ? 'Error while loading from API' - : '' + staticErrorText } options={ type === CloudPulseSelectTypes.static From d6b4d1ac708cff099f4a05df9b57002483939585 Mon Sep 17 00:00:00 2001 From: vmangalr Date: Wed, 21 Aug 2024 17:24:36 +0530 Subject: [PATCH 07/20] upcoming: [DI-20348] - Error message fix --- .../CloudPulse/shared/CloudPulseCustomSelect.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx index 30c5c0e7500..0658d7a31bf 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx @@ -178,14 +178,10 @@ export const CloudPulseCustomSelect = React.memo( // check for input prop errors if ( - CloudPulseSelectTypes.static === type && - (!options || options.length === 0) + (CloudPulseSelectTypes.static === type && + (!options || options.length === 0)) || (CloudPulseSelectTypes.dynamic === type && !apiV4QueryKey) ) { - staticErrorText = 'Pass predefined options for static select type'; - } - - if (CloudPulseSelectTypes.dynamic === type && !apiV4QueryKey) { - staticErrorText = 'Pass API Factory for dynamic select type'; + staticErrorText = 'Pass pass either options or API query key'; } const isAutoCompleteDisabled = disabled || ((isLoading || isError) && type === CloudPulseSelectTypes.dynamic) || From dfbcaa738a816848d58f263b93859d934c2b6b51 Mon Sep 17 00:00:00 2001 From: vmangalr Date: Wed, 21 Aug 2024 18:03:49 +0530 Subject: [PATCH 08/20] upcoming: [DI-20348] - Simplify checks --- .../features/CloudPulse/shared/CloudPulseCustomSelect.tsx | 8 +++----- .../CloudPulse/shared/CloudPulseCustomSelectUtils.ts | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx index 0658d7a31bf..992de21388d 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx @@ -16,7 +16,7 @@ import type { } from '../Utils/models'; /** - * This is the properties requires for CloudPulseCustomSelect Components + * These are the properties requires for CloudPulseCustomSelect Components * */ export interface CloudPulseCustomSelectProps { @@ -132,9 +132,7 @@ export const CloudPulseCustomSelect = React.memo( isLoading, } = useGetCustomFiltersQuery({ apiV4QueryKey, - enabled: - apiV4QueryKey !== undefined && - (disabled !== undefined ? !disabled : true), + enabled:Boolean(apiV4QueryKey && !disabled), filter: {}, idField: apiResponseIdField ? apiResponseIdField : 'id', labelField: apiResponseLabelField ? apiResponseLabelField : 'label', @@ -215,7 +213,7 @@ export const CloudPulseCustomSelect = React.memo( multiple={isMultiSelect} onChange={handleChange} placeholder={placeholder ?? 'Select a Value'} - value={selectedResource ? selectedResource : isMultiSelect ? [] : null} + value={ selectedResource ?? (isMultiSelect ? [] : null) } /> ); }, diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts index ac9b96318bb..d5df8dd6d20 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts @@ -116,7 +116,7 @@ export const getDefaultSelectionsFromPreferencesAndPublishSelectionChanges = ( : selectedValues[0].id : undefined // if this is multiselect, return list of ids, otherwise return single id ); - return selectedValues && selectedValues.length > 0 + return selectedValues?.length ? isMultiSelect ? selectedValues : selectedValues[0] From 0718c6859552f3395c55af0c7cb1978bedb7bce3 Mon Sep 17 00:00:00 2001 From: vmangalr Date: Wed, 21 Aug 2024 18:25:18 +0530 Subject: [PATCH 09/20] upcoming: [DI-20348] - UT's for cloudpulsecustom select --- .../CloudPulseCustomSelectUtils.test.ts | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts new file mode 100644 index 00000000000..da71837b11a --- /dev/null +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts @@ -0,0 +1,135 @@ +import { + callSelectionChangeAndUpdateGlobalFilters, + getDefaultSelectionsFromPreferencesAndPublishSelectionChanges, +} from './CloudPulseCustomSelectUtils'; + +import type { CloudPulseServiceTypeFiltersOptions } from '../Utils/models'; + +const queryMocks = vi.hoisted(() => ({ + getUserPreferenceObject: vi.fn().mockReturnValue({}), +})); + +vi.mock('../Utils/UserPreference', async () => { + const actual = await vi.importActual('../Utils/UserPreference'); + return { + ...actual, + getUserPreferenceObject: queryMocks.getUserPreferenceObject, + }; +}); + +it('test callSelectionChangeAndUpdateGlobalFilters method for single selection', () => { + const selectedValue: CloudPulseServiceTypeFiltersOptions = { + id: '1', + label: 'Test', + }; + const handleSelectionChange = vi.fn(); + const result = callSelectionChangeAndUpdateGlobalFilters({ + clearSelections: [], + filterKey: 'test', + handleSelectionChange, + value: selectedValue, + }); + + expect(result).toBeDefined(); + expect(result).toEqual(selectedValue); + expect(handleSelectionChange).toBeCalledTimes(1); +}); + +it('test callSelectionChangeAndUpdateGlobalFilters method for multiple selection', () => { + const selectedValue: CloudPulseServiceTypeFiltersOptions[] = [ + { + id: '1', + label: 'Test', + }, + ]; + const handleSelectionChange = vi.fn(); + const result = callSelectionChangeAndUpdateGlobalFilters({ + clearSelections: [], + filterKey: 'test', + handleSelectionChange, + value: selectedValue, + }); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result).toEqual(selectedValue); + expect(handleSelectionChange).toBeCalledTimes(1); +}); + +it('test getDefaultSelectionsFromPreferencesAndPublishSelectionChanges method for single selection', () => { + const handleSelectionChange = vi.fn(); + queryMocks.getUserPreferenceObject.mockReturnValue({ + test: '1', + }); + + const options: CloudPulseServiceTypeFiltersOptions[] = [ + { + id: '1', + label: 'Test', + }, + ]; + + let result = getDefaultSelectionsFromPreferencesAndPublishSelectionChanges({ + filterKey: 'test', + handleSelectionChange, + isMultiSelect: false, + options, + savePreferences: true, + }); + + expect(Array.isArray(result)).toBe(false); + expect(result).toEqual(options[0]); + expect(handleSelectionChange).toBeCalledTimes(1); + queryMocks.getUserPreferenceObject.mockReturnValue({ + test: '2', + }); + + result = getDefaultSelectionsFromPreferencesAndPublishSelectionChanges({ + filterKey: 'test', + handleSelectionChange, + isMultiSelect: false, + options, + savePreferences: true, + }); + expect(result).toEqual(undefined); + expect(handleSelectionChange).toBeCalledTimes(2); +}); + +it('test getDefaultSelectionsFromPreferencesAndPublishSelectionChanges method for multi selection', () => { + const handleSelectionChange = vi.fn(); + queryMocks.getUserPreferenceObject.mockReturnValue({ + test: '1', + }); + + const options: CloudPulseServiceTypeFiltersOptions[] = [ + { + id: '1', + label: 'Test', + }, + ]; + + let result = getDefaultSelectionsFromPreferencesAndPublishSelectionChanges({ + filterKey: 'test', + handleSelectionChange, + isMultiSelect: true, + options, + savePreferences: true, + }); + + expect(Array.isArray(result)).toBe(true); + expect(result).toEqual(options); + expect(handleSelectionChange).toBeCalledTimes(1); + queryMocks.getUserPreferenceObject.mockReturnValue({ + test: '2', + }); + + result = getDefaultSelectionsFromPreferencesAndPublishSelectionChanges({ + filterKey: 'test', + handleSelectionChange, + isMultiSelect: false, + options, + savePreferences: true, + }); + expect(result).toEqual(undefined); + expect(handleSelectionChange).toBeCalledTimes(2); +}); From ab7030ebed0c0fe5e769190eb6521555ded57b5b Mon Sep 17 00:00:00 2001 From: vmangalr Date: Wed, 21 Aug 2024 18:54:58 +0530 Subject: [PATCH 10/20] upcoming: [DI-20348] - UT updates --- .../CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts index da71837b11a..af16d49b57b 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts @@ -6,7 +6,9 @@ import { import type { CloudPulseServiceTypeFiltersOptions } from '../Utils/models'; const queryMocks = vi.hoisted(() => ({ - getUserPreferenceObject: vi.fn().mockReturnValue({}), + getUserPreferenceObject: vi.fn().mockReturnValue({ + test: '1', + }), })); vi.mock('../Utils/UserPreference', async () => { @@ -58,9 +60,6 @@ it('test callSelectionChangeAndUpdateGlobalFilters method for multiple selection it('test getDefaultSelectionsFromPreferencesAndPublishSelectionChanges method for single selection', () => { const handleSelectionChange = vi.fn(); - queryMocks.getUserPreferenceObject.mockReturnValue({ - test: '1', - }); const options: CloudPulseServiceTypeFiltersOptions[] = [ { @@ -97,6 +96,7 @@ it('test getDefaultSelectionsFromPreferencesAndPublishSelectionChanges method fo it('test getDefaultSelectionsFromPreferencesAndPublishSelectionChanges method for multi selection', () => { const handleSelectionChange = vi.fn(); + queryMocks.getUserPreferenceObject.mockReturnValue({ test: '1', }); From 4c337b36477e83e5173e69236329c6247dbe291f Mon Sep 17 00:00:00 2001 From: vmangalr Date: Thu, 22 Aug 2024 08:01:01 +0530 Subject: [PATCH 11/20] upcoming: [DI-20348] - PR comments changes --- .../CloudPulse/shared/CloudPulseCustomSelect.tsx | 12 ++++++------ .../shared/CloudPulseCustomSelectUtils.test.ts | 16 ++++++++-------- .../shared/CloudPulseCustomSelectUtils.ts | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx index 992de21388d..adb5509ed75 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx @@ -5,8 +5,8 @@ import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { useGetCustomFiltersQuery } from 'src/queries/cloudpulse/customfilters'; import { - callSelectionChangeAndUpdateGlobalFilters, - getDefaultSelectionsFromPreferencesAndPublishSelectionChanges, + handleCustomSelectionChange, + getInitialDefaultSelections, } from './CloudPulseCustomSelectUtils'; import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding'; @@ -141,7 +141,7 @@ export const CloudPulseCustomSelect = React.memo( React.useEffect(() => { if (!selectedResource) { setResource( - getDefaultSelectionsFromPreferencesAndPublishSelectionChanges( + getInitialDefaultSelections( { filterKey, handleSelectionChange, @@ -162,14 +162,14 @@ export const CloudPulseCustomSelect = React.memo( | CloudPulseServiceTypeFiltersOptions[] | null ) => { - const filterdValue = callSelectionChangeAndUpdateGlobalFilters({ + const filteredValue = handleCustomSelectionChange({ clearSelections: clearDependentSelections ?? [], filterKey, handleSelectionChange, maxSelections, value, }); - setResource(Array.isArray(filterdValue) ? [...filterdValue] : filterdValue ?? undefined); + setResource(Array.isArray(filteredValue) ? [...filteredValue] : filteredValue ?? undefined); }; let staticErrorText = ''; @@ -179,7 +179,7 @@ export const CloudPulseCustomSelect = React.memo( (CloudPulseSelectTypes.static === type && (!options || options.length === 0)) || (CloudPulseSelectTypes.dynamic === type && !apiV4QueryKey) ) { - staticErrorText = 'Pass pass either options or API query key'; + staticErrorText = 'Pass either options or API query key'; } const isAutoCompleteDisabled = disabled || ((isLoading || isError) && type === CloudPulseSelectTypes.dynamic) || diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts index af16d49b57b..353fd3f1bdd 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts @@ -1,6 +1,6 @@ import { - callSelectionChangeAndUpdateGlobalFilters, - getDefaultSelectionsFromPreferencesAndPublishSelectionChanges, + handleCustomSelectionChange, + getInitialDefaultSelections, } from './CloudPulseCustomSelectUtils'; import type { CloudPulseServiceTypeFiltersOptions } from '../Utils/models'; @@ -25,7 +25,7 @@ it('test callSelectionChangeAndUpdateGlobalFilters method for single selection', label: 'Test', }; const handleSelectionChange = vi.fn(); - const result = callSelectionChangeAndUpdateGlobalFilters({ + const result = handleCustomSelectionChange({ clearSelections: [], filterKey: 'test', handleSelectionChange, @@ -45,7 +45,7 @@ it('test callSelectionChangeAndUpdateGlobalFilters method for multiple selection }, ]; const handleSelectionChange = vi.fn(); - const result = callSelectionChangeAndUpdateGlobalFilters({ + const result = handleCustomSelectionChange({ clearSelections: [], filterKey: 'test', handleSelectionChange, @@ -68,7 +68,7 @@ it('test getDefaultSelectionsFromPreferencesAndPublishSelectionChanges method fo }, ]; - let result = getDefaultSelectionsFromPreferencesAndPublishSelectionChanges({ + let result = getInitialDefaultSelections({ filterKey: 'test', handleSelectionChange, isMultiSelect: false, @@ -83,7 +83,7 @@ it('test getDefaultSelectionsFromPreferencesAndPublishSelectionChanges method fo test: '2', }); - result = getDefaultSelectionsFromPreferencesAndPublishSelectionChanges({ + result = getInitialDefaultSelections({ filterKey: 'test', handleSelectionChange, isMultiSelect: false, @@ -108,7 +108,7 @@ it('test getDefaultSelectionsFromPreferencesAndPublishSelectionChanges method fo }, ]; - let result = getDefaultSelectionsFromPreferencesAndPublishSelectionChanges({ + let result = getInitialDefaultSelections({ filterKey: 'test', handleSelectionChange, isMultiSelect: true, @@ -123,7 +123,7 @@ it('test getDefaultSelectionsFromPreferencesAndPublishSelectionChanges method fo test: '2', }); - result = getDefaultSelectionsFromPreferencesAndPublishSelectionChanges({ + result = getInitialDefaultSelections({ filterKey: 'test', handleSelectionChange, isMultiSelect: false, diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts index d5df8dd6d20..ced2be7b762 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts @@ -71,7 +71,7 @@ interface CloudPulseCustomSelectionChangeProps { * @param defaultSelectionProps - The props needed for getting the default selections * @returns */ -export const getDefaultSelectionsFromPreferencesAndPublishSelectionChanges = ( +export const getInitialDefaultSelections = ( defaultSelectionProps: CloudPulseCustomSelectDefaultValueProps ): | CloudPulseServiceTypeFiltersOptions @@ -128,7 +128,7 @@ export const getDefaultSelectionsFromPreferencesAndPublishSelectionChanges = ( * @param selectionChangeProps - The props needed for selecting the new filter and updating the global preferences */ -export const callSelectionChangeAndUpdateGlobalFilters = ( +export const handleCustomSelectionChange = ( selectionChangeProps: CloudPulseCustomSelectionChangeProps ): | CloudPulseServiceTypeFiltersOptions From 4ab744914e5311d8dbd3a0e5521fe6abc79ec223 Mon Sep 17 00:00:00 2001 From: vmangalr Date: Thu, 22 Aug 2024 08:03:01 +0530 Subject: [PATCH 12/20] upcoming: [DI-20348] - Simple names for functions --- .../CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx | 4 ++-- .../src/features/CloudPulse/Utils/FilterBuilder.test.ts | 6 +++--- .../manager/src/features/CloudPulse/Utils/FilterBuilder.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx index 7addf6c414e..ab606dfcd34 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx @@ -9,7 +9,7 @@ import { GlobalFilters } from '../Overview/GlobalFilters'; import { REGION, RESOURCE_ID } from '../Utils/constants'; import { checkIfAllMandatoryFiltersAreSelected, - getFiltersForMetricsCallFromCustomSelect, + getMetricsCallCustomFilters, } from '../Utils/FilterBuilder'; import { FILTER_CONFIG } from '../Utils/FilterConfig'; import { useLoadUserPreferences } from '../Utils/UserPreference'; @@ -100,7 +100,7 @@ export const CloudPulseDashboardLanding = () => { return ( { }); it('test getFiltersForMetricsCallFromCustomSelect method', () => { - let result = getFiltersForMetricsCallFromCustomSelect({ + let result = getMetricsCallCustomFilters({ 'resource_id' : [1,2,3] }, 'linode'); @@ -242,7 +242,7 @@ it('test getFiltersForMetricsCallFromCustomSelect method', () => { }); it('test constructAdditionalRequestFilters method', () => { - let result = constructAdditionalRequestFilters(getFiltersForMetricsCallFromCustomSelect({ + let result = constructAdditionalRequestFilters(getMetricsCallCustomFilters({ 'resource_id' : [1,2,3] }, 'linode')) diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts index 9c8c0033a84..67b05e4e1db 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts @@ -259,7 +259,7 @@ export const checkIfAllMandatoryFiltersAreSelected = ( * @param serviceType The serviceType assosicated with the dashboard like linode, dbaas etc., * @returns Constructs and returns the metrics call filters based on selected filters and service type */ -export const getFiltersForMetricsCallFromCustomSelect = ( +export const getMetricsCallCustomFilters = ( selectedFilters: { [key: string]: FilterValueType; }, From 3713ff870c0847f0f7b07f47fda8bc1ee5e28445 Mon Sep 17 00:00:00 2001 From: vmangalr Date: Thu, 22 Aug 2024 16:15:25 +0530 Subject: [PATCH 13/20] upcoming: [DI-20348] - JSDoc comments fix --- .../src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx | 2 +- .../features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx index adb5509ed75..f5e6550cb32 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx @@ -31,7 +31,7 @@ export interface CloudPulseCustomSelectProps { apiResponseLabelField?: string; /** - * The api URL which contains the list of filters, passed when the select type is dynamic + * The api query key factory which contains the queries to fetch the list of filters, passed when the select type is dynamic */ apiV4QueryKey?: QueryFunctionAndKey; diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts index ced2be7b762..d0d9b7f2094 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts @@ -69,7 +69,6 @@ interface CloudPulseCustomSelectionChangeProps { /** * This function returns the default selections based on the user preference and options listed * @param defaultSelectionProps - The props needed for getting the default selections - * @returns */ export const getInitialDefaultSelections = ( defaultSelectionProps: CloudPulseCustomSelectDefaultValueProps From f8021f107d8d4ba3b5cf6383bc59a20c5bfec306 Mon Sep 17 00:00:00 2001 From: vmangalr Date: Thu, 22 Aug 2024 18:41:44 +0530 Subject: [PATCH 14/20] upcoming: [DI-20348] - Lint issue fix --- .../src/features/CloudPulse/Utils/FilterBuilder.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts index 4fd6cadb27e..f9605d26243 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts @@ -193,7 +193,7 @@ it('test checkIfAllMandatoryFiltersAreSelected method', () => { }); it('test getCustomSelectProperties method', () => { - let customSelectEngineConfig = dbaasConfig?.filters.find( + const customSelectEngineConfig = dbaasConfig?.filters.find( (filterObj) => filterObj.name === 'DB Engine' ); @@ -233,7 +233,7 @@ it('test getCustomSelectProperties method', () => { }); it('test getFiltersForMetricsCallFromCustomSelect method', () => { - let result = getMetricsCallCustomFilters({ + const result = getMetricsCallCustomFilters({ 'resource_id' : [1,2,3] }, 'linode'); @@ -242,7 +242,7 @@ it('test getFiltersForMetricsCallFromCustomSelect method', () => { }); it('test constructAdditionalRequestFilters method', () => { - let result = constructAdditionalRequestFilters(getMetricsCallCustomFilters({ + const result = constructAdditionalRequestFilters(getMetricsCallCustomFilters({ 'resource_id' : [1,2,3] }, 'linode')) From 88509a135a73191379a69d4128747f91f98b078a Mon Sep 17 00:00:00 2001 From: vmangalr Date: Fri, 23 Aug 2024 16:03:09 +0530 Subject: [PATCH 15/20] upcoming: [DI-20348] - code simplification --- .../src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx index f5e6550cb32..a4c813e386f 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx @@ -134,8 +134,8 @@ export const CloudPulseCustomSelect = React.memo( apiV4QueryKey, enabled:Boolean(apiV4QueryKey && !disabled), filter: {}, - idField: apiResponseIdField ? apiResponseIdField : 'id', - labelField: apiResponseLabelField ? apiResponseLabelField : 'label', + idField: apiResponseIdField ?? 'id', + labelField: apiResponseLabelField ?? 'label', }); React.useEffect(() => { From a8973b8183b10f6b3eceecbde38604e46f89d4f7 Mon Sep 17 00:00:00 2001 From: vmangalr Date: Sat, 24 Aug 2024 07:29:04 +0530 Subject: [PATCH 16/20] upcoming: [DI-20348] - PR comments and changeset updates --- .../.changeset/pr-10807-upcoming-features-1724226251911.md | 2 +- .../manager/src/features/CloudPulse/Utils/FilterBuilder.ts | 6 +++--- .../CloudPulse/shared/CloudPulseCustomSelectUtils.ts | 6 +++--- packages/manager/src/mocks/serverHandlers.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/manager/.changeset/pr-10807-upcoming-features-1724226251911.md b/packages/manager/.changeset/pr-10807-upcoming-features-1724226251911.md index c8188d70991..b710ea68af5 100644 --- a/packages/manager/.changeset/pr-10807-upcoming-features-1724226251911.md +++ b/packages/manager/.changeset/pr-10807-upcoming-features-1724226251911.md @@ -2,4 +2,4 @@ "@linode/manager": Upcoming Features --- -Add new CloudPulseCustomSelect component and integrate with the global filter builder. ([#10807](https://github.com/linode/manager/pull/10807)) +Add new CloudPulseCustomSelect component and integrate with the global filter builder ([#10807](https://github.com/linode/manager/pull/10807)) diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts index 67b05e4e1db..1b28141456a 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts @@ -90,7 +90,7 @@ export const getResourcesProperties = ( /** * @param props The cloudpulse filter properties selected so far - * @param handleCustomSelectChange The call back function when a filter change happens + * @param handleCustomSelectChange The callback function when a filter change happens * @returns {CloudPulseCustomSelectProps} Returns a property compatible for CloudPulseCustomSelect Component */ export const getCustomSelectProperties = ( @@ -129,7 +129,7 @@ export const getCustomSelectProperties = ( type: options ? CloudPulseSelectTypes.static : CloudPulseSelectTypes.dynamic, - clearDependentSelections: getDependantFiltersByFilterKey(filterKey, dashboard), + clearDependentSelections: getDependentFiltersByFilterKey(filterKey, dashboard), }; }; @@ -309,7 +309,7 @@ export const constructAdditionalRequestFilters = ( * @param dashboard The selected dashboard from the global filter view * @returns The filterKeys that needs to be removed from the preferences */ -const getDependantFiltersByFilterKey = +const getDependentFiltersByFilterKey = (filterKey: string, dashboard: Dashboard): string[] => { const serviceTypeConfig = FILTER_CONFIG.get(dashboard.service_type); diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts index d0d9b7f2094..4bd030e8627 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts @@ -20,7 +20,7 @@ interface CloudPulseCustomSelectDefaultValueProps { handleSelectionChange: (filterKey: string, value: FilterValueType) => void; /** - * This indicates whether we need multiselect for the component or not + * Indicates whether we need multiselect for the component or not */ isMultiSelect: boolean; @@ -30,7 +30,7 @@ interface CloudPulseCustomSelectDefaultValueProps { options: CloudPulseServiceTypeFiltersOptions[]; /** - * This indicates whether we need to save preferences or not + * Indicates whether we need to save preferences or not */ savePreferences: boolean; } @@ -152,7 +152,7 @@ export const handleCustomSelectionChange = ( : String(value.id) : undefined; - // pubish the selection change + // publish the selection change handleSelectionChange(filterKey, result); // update the preferences diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index f46e627b28a..6996ae83709 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -2276,7 +2276,7 @@ export const handlers = [ service_type: 'linode' }), dashboardFactory.build({ - label: 'Dbaas Dashboard', + label: 'DBaaS Dashboard', service_type: 'dbaas' }) ], From 7d3e1b3366a7ed634529c8d0dafbfee7adca8bc8 Mon Sep 17 00:00:00 2001 From: vmangalr Date: Sat, 24 Aug 2024 07:37:52 +0530 Subject: [PATCH 17/20] upcoming: [DI-20348] - ESlint issue fixes across files --- .../Dashboard/CloudPulseDashboard.tsx | 4 +- .../Dashboard/CloudPulseDashboardLanding.tsx | 2 +- .../CloudPulse/Utils/FilterBuilder.test.ts | 57 ++++++++++------ .../CloudPulse/Utils/FilterBuilder.ts | 65 ++++++++++--------- .../CloudPulse/Widget/CloudPulseWidget.tsx | 8 +-- .../shared/CloudPulseComponentRenderer.tsx | 5 +- .../shared/CloudPulseCustomSelect.tsx | 62 +++++++++--------- .../CloudPulseDashboardFilterBuilder.tsx | 2 +- .../src/queries/cloudpulse/customfilters.ts | 2 +- 9 files changed, 116 insertions(+), 91 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx index bb2f9ad310f..ac019a321d3 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx @@ -72,12 +72,12 @@ export interface DashboardProperties { export const CloudPulseDashboard = (props: DashboardProperties) => { const { + additionalFilters, dashboardId, duration, manualRefreshTimeStamp, resources, savePref, - additionalFilters, } = props; const getJweTokenPayload = (): JWETokenPayLoad => { @@ -248,4 +248,4 @@ export const CloudPulseDashboard = (props: DashboardProperties) => { }; return ; -}; \ No newline at end of file +}; diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx index ab606dfcd34..89f5f6db2c9 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx @@ -139,4 +139,4 @@ export const CloudPulseDashboardLanding = () => { ); -}; \ No newline at end of file +}; diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts index f9605d26243..1c4b5894d7d 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts @@ -1,5 +1,7 @@ import { dashboardFactory } from 'src/factories'; +import { databaseQueries } from 'src/queries/databases/databases'; +import { RESOURCES } from './constants'; import { buildXFilter, checkIfAllMandatoryFiltersAreSelected, @@ -13,8 +15,6 @@ import { } from './FilterBuilder'; import { FILTER_CONFIG } from './FilterConfig'; import { CloudPulseSelectTypes } from './models'; -import { databaseQueries } from 'src/queries/databases/databases'; -import { RESOURCES } from './constants'; const mockDashboard = dashboardFactory.build(); @@ -199,12 +199,15 @@ it('test getCustomSelectProperties method', () => { expect(customSelectEngineConfig).toBeDefined(); - if(customSelectEngineConfig) { - let result = getCustomSelectProperties({ - config : customSelectEngineConfig, - dashboard: {...mockDashboard, service_type:'dbaas'}, + if (customSelectEngineConfig) { + let result = getCustomSelectProperties( + { + config: customSelectEngineConfig, + dashboard: { ...mockDashboard, service_type: 'dbaas' }, isServiceAnalyticsIntegration: true, - }, vi.fn()); + }, + vi.fn() + ); expect(result.options).toBeDefined(); expect(result.options?.length).toEqual(2); @@ -215,15 +218,19 @@ it('test getCustomSelectProperties method', () => { expect(result.clearDependentSelections?.includes(RESOURCES)).toBe(true); customSelectEngineConfig.configuration.type = CloudPulseSelectTypes.dynamic; - customSelectEngineConfig.configuration.apiV4QueryKey = databaseQueries.engines; + customSelectEngineConfig.configuration.apiV4QueryKey = + databaseQueries.engines; customSelectEngineConfig.configuration.isMultiSelect = true; customSelectEngineConfig.configuration.options = undefined; - result = getCustomSelectProperties({ - config : customSelectEngineConfig, - dashboard: mockDashboard, - isServiceAnalyticsIntegration: true, - }, vi.fn()); + result = getCustomSelectProperties( + { + config: customSelectEngineConfig, + dashboard: mockDashboard, + isServiceAnalyticsIntegration: true, + }, + vi.fn() + ); expect(result.apiV4QueryKey).toEqual(databaseQueries.engines); expect(result.type).toEqual(CloudPulseSelectTypes.dynamic); @@ -233,18 +240,26 @@ it('test getCustomSelectProperties method', () => { }); it('test getFiltersForMetricsCallFromCustomSelect method', () => { - const result = getMetricsCallCustomFilters({ - 'resource_id' : [1,2,3] - }, 'linode'); + const result = getMetricsCallCustomFilters( + { + resource_id: [1, 2, 3], + }, + 'linode' + ); - expect(result).toBeDefined(); - expect(result.length).toEqual(0); + expect(result).toBeDefined(); + expect(result.length).toEqual(0); }); it('test constructAdditionalRequestFilters method', () => { - const result = constructAdditionalRequestFilters(getMetricsCallCustomFilters({ - 'resource_id' : [1,2,3] - }, 'linode')) + const result = constructAdditionalRequestFilters( + getMetricsCallCustomFilters( + { + resource_id: [1, 2, 3], + }, + 'linode' + ) + ); expect(result).toBeDefined(); expect(result.length).toEqual(0); diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts index 1b28141456a..57a6b6e7bbc 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts @@ -1,6 +1,6 @@ import { RELATIVE_TIME_DURATION, RESOURCE_ID, RESOURCES } from './constants'; import { FILTER_CONFIG } from './FilterConfig'; -import { CloudPulseSelectTypes, type CloudPulseServiceTypeFilters } from './models'; +import { CloudPulseSelectTypes } from './models'; import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding'; import type { CloudPulseCustomSelectProps } from '../shared/CloudPulseCustomSelect'; @@ -10,8 +10,9 @@ import type { CloudPulseResourcesSelectProps, } from '../shared/CloudPulseResourcesSelect'; import type { CloudPulseTimeRangeSelectProps } from '../shared/CloudPulseTimeRangeSelect'; +import type { CloudPulseMetricsAdditionalFilters } from '../Widget/CloudPulseWidget'; +import type { CloudPulseServiceTypeFilters } from './models'; import type { Dashboard, Filter, Filters, TimeDuration } from '@linode/api-v4'; -import { CloudPulseMetricsAdditionalFilters } from '../Widget/CloudPulseWidget'; interface CloudPulseFilterProperties { config: CloudPulseServiceTypeFilters; @@ -113,6 +114,10 @@ export const getCustomSelectProperties = ( apiResponseIdField: apiIdField, apiResponseLabelField: apiLabelField, apiV4QueryKey, + clearDependentSelections: getDependentFiltersByFilterKey( + filterKey, + dashboard + ), disabled: checkIfWeNeedToDisableFilterByFilterKey( filterKey, dependentFilters ?? {}, @@ -129,7 +134,6 @@ export const getCustomSelectProperties = ( type: options ? CloudPulseSelectTypes.static : CloudPulseSelectTypes.dynamic, - clearDependentSelections: getDependentFiltersByFilterKey(filterKey, dashboard), }; }; @@ -268,16 +272,19 @@ export const getMetricsCallCustomFilters = ( const serviceTypeConfig = FILTER_CONFIG.get(serviceType); // If configuration exists, filter and map it to the desired CloudPulseMetricsAdditionalFilters format - return serviceTypeConfig?.filters - .filter(({ configuration }) => - configuration.isFilterable && !configuration.isMetricsFilter && + return ( + serviceTypeConfig?.filters + .filter( + ({ configuration }) => + configuration.isFilterable && + !configuration.isMetricsFilter && selectedFilters[configuration.filterKey] - ) - .map(({ configuration }) => ({ - filterKey: configuration.filterKey, - filterValue: selectedFilters[configuration.filterKey], - })) - ?? []; + ) + .map(({ configuration }) => ({ + filterKey: configuration.filterKey, + filterValue: selectedFilters[configuration.filterKey], + })) ?? [] + ); }; /** @@ -304,26 +311,26 @@ export const constructAdditionalRequestFilters = ( }; /** - * + * * @param filterKey The filterKey of the actual filter * @param dashboard The selected dashboard from the global filter view * @returns The filterKeys that needs to be removed from the preferences */ -const getDependentFiltersByFilterKey = - (filterKey: string, dashboard: Dashboard): string[] => { - const serviceTypeConfig = FILTER_CONFIG.get(dashboard.service_type); +const getDependentFiltersByFilterKey = ( + filterKey: string, + dashboard: Dashboard +): string[] => { + const serviceTypeConfig = FILTER_CONFIG.get(dashboard.service_type); - if (!serviceTypeConfig) { - return []; - } + if (!serviceTypeConfig) { + return []; + } - return serviceTypeConfig.filters - .filter((filter) => - filter?.configuration?.dependency?.includes(filterKey) - ) - .map(({configuration}) => - configuration.filterKey === RESOURCE_ID - ? RESOURCES - : configuration.filterKey - ); - }; + return serviceTypeConfig.filters + .filter((filter) => filter?.configuration?.dependency?.includes(filterKey)) + .map(({ configuration }) => + configuration.filterKey === RESOURCE_ID + ? RESOURCES + : configuration.filterKey + ); +}; diff --git a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx index 09b6ecabb20..a7172590950 100644 --- a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx @@ -12,6 +12,7 @@ import { getCloudPulseMetricRequest, } from '../Utils/CloudPulseWidgetUtils'; import { AGGREGATE_FUNCTION, SIZE, TIME_GRANULARITY } from '../Utils/constants'; +import { constructAdditionalRequestFilters } from '../Utils/FilterBuilder'; import { convertValueToUnit, formatToolTip } from '../Utils/unitConversion'; import { getUserPreferenceObject, @@ -25,15 +26,14 @@ import { ZoomIcon } from './components/Zoomer'; import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding'; import type { CloudPulseResources } from '../shared/CloudPulseResourcesSelect'; +import type { Widgets } from '@linode/api-v4'; import type { AvailableMetrics, TimeDuration, TimeGranularity, } from '@linode/api-v4'; -import type { Widgets } from '@linode/api-v4'; import type { DataSet } from 'src/components/LineGraph/LineGraph'; import type { Metrics } from 'src/utilities/statMetrics'; -import { constructAdditionalRequestFilters } from '../Utils/FilterBuilder'; export interface CloudPulseWidgetProperties { /** @@ -127,6 +127,7 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { const [widget, setWidget] = React.useState({ ...props.widget }); const { + additionalFilters, ariaLabel, authToken, availableMetrics, @@ -137,7 +138,6 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { serviceType, timeStamp, unit, - additionalFilters, } = props; const flags = useFlags(); @@ -360,4 +360,4 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { ); -}; \ No newline at end of file +}; diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.tsx index f4d5a7f2e27..224956a45ca 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.tsx @@ -2,11 +2,12 @@ import React from 'react'; import NullComponent from 'src/components/NullComponent'; -import { CloudPulseCustomSelect, type CloudPulseCustomSelectProps } from './CloudPulseCustomSelect'; +import { CloudPulseCustomSelect } from './CloudPulseCustomSelect'; import { CloudPulseRegionSelect } from './CloudPulseRegionSelect'; import { CloudPulseResourcesSelect } from './CloudPulseResourcesSelect'; import { CloudPulseTimeRangeSelect } from './CloudPulseTimeRangeSelect'; +import type { CloudPulseCustomSelectProps } from './CloudPulseCustomSelect'; import type { CloudPulseRegionSelectProps } from './CloudPulseRegionSelect'; import type { CloudPulseResourcesSelectProps } from './CloudPulseResourcesSelect'; import type { CloudPulseTimeRangeSelectProps } from './CloudPulseTimeRangeSelect'; @@ -48,4 +49,4 @@ const buildComponent = (props: CloudPulseComponentRendererProps) => { return ; }; -export default buildComponent; \ No newline at end of file +export default buildComponent; diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx index a4c813e386f..f43f0c8c74f 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx @@ -5,8 +5,8 @@ import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { useGetCustomFiltersQuery } from 'src/queries/cloudpulse/customfilters'; import { - handleCustomSelectionChange, getInitialDefaultSelections, + handleCustomSelectionChange, } from './CloudPulseCustomSelectUtils'; import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding'; @@ -132,24 +132,22 @@ export const CloudPulseCustomSelect = React.memo( isLoading, } = useGetCustomFiltersQuery({ apiV4QueryKey, - enabled:Boolean(apiV4QueryKey && !disabled), + enabled: Boolean(apiV4QueryKey && !disabled), filter: {}, idField: apiResponseIdField ?? 'id', labelField: apiResponseLabelField ?? 'label', }); React.useEffect(() => { - if (!selectedResource) { + if (!selectedResource) { setResource( - getInitialDefaultSelections( - { - filterKey, - handleSelectionChange, - isMultiSelect: isMultiSelect ?? false, - options: options ?? [], - savePreferences: savePreferences ?? false, - } - ) + getInitialDefaultSelections({ + filterKey, + handleSelectionChange, + isMultiSelect: isMultiSelect ?? false, + options: options ?? [], + savePreferences: savePreferences ?? false, + }) ); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -169,7 +167,11 @@ export const CloudPulseCustomSelect = React.memo( maxSelections, value, }); - setResource(Array.isArray(filteredValue) ? [...filteredValue] : filteredValue ?? undefined); + setResource( + Array.isArray(filteredValue) + ? [...filteredValue] + : filteredValue ?? undefined + ); }; let staticErrorText = ''; @@ -177,29 +179,27 @@ export const CloudPulseCustomSelect = React.memo( // check for input prop errors if ( (CloudPulseSelectTypes.static === type && - (!options || options.length === 0)) || (CloudPulseSelectTypes.dynamic === type && !apiV4QueryKey) + (!options || options.length === 0)) || + (CloudPulseSelectTypes.dynamic === type && !apiV4QueryKey) ) { staticErrorText = 'Pass either options or API query key'; } - const isAutoCompleteDisabled = disabled || ((isLoading || isError) && type === CloudPulseSelectTypes.dynamic) || - (!queriedResources && !(options && options.length)) || - staticErrorText.length > 0; + const isAutoCompleteDisabled = + disabled || + ((isLoading || isError) && type === CloudPulseSelectTypes.dynamic) || + (!queriedResources && !(options && options.length)) || + staticErrorText.length > 0; - staticErrorText = staticErrorText.length > 0 - ? staticErrorText - : isError - ? 'Error while loading from API' - : ''; + staticErrorText = + staticErrorText.length > 0 + ? staticErrorText + : isError + ? 'Error while loading from API' + : ''; return ( option.label === value.label} label="Select a Value" multiple={isMultiSelect} onChange={handleChange} placeholder={placeholder ?? 'Select a Value'} - value={ selectedResource ?? (isMultiSelect ? [] : null) } + value={selectedResource ?? (isMultiSelect ? [] : null)} /> ); }, @@ -243,4 +245,4 @@ function compareProps( // Ignore function props in comparison return true; -} \ No newline at end of file +} diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx index c5b4bc4d0c5..a0e1cd76801 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx @@ -254,4 +254,4 @@ function compareProps( newProps: CloudPulseDashboardFilterBuilderProps ) { return oldProps.dashboard?.id === newProps.dashboard?.id; -} \ No newline at end of file +} diff --git a/packages/manager/src/queries/cloudpulse/customfilters.ts b/packages/manager/src/queries/cloudpulse/customfilters.ts index 5b0ba5035e0..72ea43af0c4 100644 --- a/packages/manager/src/queries/cloudpulse/customfilters.ts +++ b/packages/manager/src/queries/cloudpulse/customfilters.ts @@ -90,4 +90,4 @@ const getStringValue = ( } } return undefined; -}; \ No newline at end of file +}; From 70a787d24f80572f0eff776c86f338cab50e56a1 Mon Sep 17 00:00:00 2001 From: vmangalr Date: Mon, 26 Aug 2024 08:03:02 +0530 Subject: [PATCH 18/20] upcoming: [DI-20348] - ESlint issue fixes across files --- .../CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts index 353fd3f1bdd..dee5cfb1835 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts @@ -1,6 +1,6 @@ import { - handleCustomSelectionChange, getInitialDefaultSelections, + handleCustomSelectionChange, } from './CloudPulseCustomSelectUtils'; import type { CloudPulseServiceTypeFiltersOptions } from '../Utils/models'; From 4dd3c6bf11a6321c1d3cec4ca6ccba5734a7d553 Mon Sep 17 00:00:00 2001 From: vmangalr Date: Mon, 26 Aug 2024 08:05:10 +0530 Subject: [PATCH 19/20] upcoming: [DI-20348] - formatting serverhandler --- packages/manager/src/mocks/serverHandlers.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 6996ae83709..4c7a494b66a 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -2272,13 +2272,13 @@ export const handlers = [ const response = { data: [ dashboardFactory.build({ - label:'Linode Dashboard', - service_type: 'linode' + label: 'Linode Dashboard', + service_type: 'linode', }), dashboardFactory.build({ label: 'DBaaS Dashboard', - service_type: 'dbaas' - }) + service_type: 'dbaas', + }), ], }; From 89bbdedd1d48d84929f0824b91ee46e8d293f243 Mon Sep 17 00:00:00 2001 From: vmangalr Date: Mon, 26 Aug 2024 16:34:18 +0530 Subject: [PATCH 20/20] upcoming: [DI-20348] - Test names updates --- .../CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts index dee5cfb1835..0c1bc3e1844 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts @@ -19,7 +19,7 @@ vi.mock('../Utils/UserPreference', async () => { }; }); -it('test callSelectionChangeAndUpdateGlobalFilters method for single selection', () => { +it('test handleCustomSelectionChange method for single selection', () => { const selectedValue: CloudPulseServiceTypeFiltersOptions = { id: '1', label: 'Test', @@ -37,7 +37,7 @@ it('test callSelectionChangeAndUpdateGlobalFilters method for single selection', expect(handleSelectionChange).toBeCalledTimes(1); }); -it('test callSelectionChangeAndUpdateGlobalFilters method for multiple selection', () => { +it('test handleCustomSelectionChange method for multiple selection', () => { const selectedValue: CloudPulseServiceTypeFiltersOptions[] = [ { id: '1', @@ -58,7 +58,7 @@ it('test callSelectionChangeAndUpdateGlobalFilters method for multiple selection expect(handleSelectionChange).toBeCalledTimes(1); }); -it('test getDefaultSelectionsFromPreferencesAndPublishSelectionChanges method for single selection', () => { +it('test getInitialDefaultSelections method for single selection', () => { const handleSelectionChange = vi.fn(); const options: CloudPulseServiceTypeFiltersOptions[] = [ @@ -94,7 +94,7 @@ it('test getDefaultSelectionsFromPreferencesAndPublishSelectionChanges method fo expect(handleSelectionChange).toBeCalledTimes(2); }); -it('test getDefaultSelectionsFromPreferencesAndPublishSelectionChanges method for multi selection', () => { +it('test getInitialDefaultSelections method for multi selection', () => { const handleSelectionChange = vi.fn(); queryMocks.getUserPreferenceObject.mockReturnValue({