From 77b780983d2e2cf23145452961d411f18c5b6380 Mon Sep 17 00:00:00 2001 From: Tracy French Date: Fri, 24 May 2024 16:39:57 -0600 Subject: [PATCH] chore: improve resource explorer stories --- .../internal-asset-explorer.tsx | 2 +- .../asset-explorer/use-assets/use-assets.ts | 14 +- .../internal-time-series-explorer.tsx | 2 +- .../use-multiple-list-requests/types.ts | 9 + .../use-cached-resources.ts | 14 +- .../use-multiple-list-requests.ts | 12 +- .../testing/helpers/selectors.ts | 25 --- .../testing/helpers/table.tsx | 23 +++ .../table-variant/asset-model-table.spec.tsx | 2 +- .../asset-property-table.spec.tsx | 28 ++- .../table-variant/asset-table.spec.tsx | 28 ++- .../table-variant/time-series-table.spec.tsx | 2 +- .../types/resource-explorer.ts | 2 +- .../resource-table/resource-table-search.tsx | 7 + .../asset-explorer.stories.tsx | 144 ++++++++-------- .../asset-model-explorer.stories.tsx | 149 ++++++++++------ .../asset-property-explorer.stories.tsx | 147 ++++++++++------ .../stories/resource-explorers/constants.ts | 44 ----- .../stories/resource-explorers/controls.ts | 78 +++++++++ .../stories/resource-explorers/decorators.tsx | 32 ++++ .../explorer-combinations.stories.tsx | 163 ++++++++++++++++-- .../time-series-explorer.stories.tsx | 162 ++++++++++------- .../stories/resource-explorers/types.ts | 10 -- 23 files changed, 731 insertions(+), 368 deletions(-) create mode 100644 packages/react-components/src/components/resource-explorers/requests/use-multiple-list-requests/types.ts delete mode 100644 packages/react-components/src/components/resource-explorers/testing/helpers/selectors.ts delete mode 100644 packages/react-components/stories/resource-explorers/constants.ts create mode 100644 packages/react-components/stories/resource-explorers/controls.ts create mode 100644 packages/react-components/stories/resource-explorers/decorators.tsx delete mode 100644 packages/react-components/stories/resource-explorers/types.ts diff --git a/packages/react-components/src/components/resource-explorers/explorers/asset-explorer/internal-asset-explorer.tsx b/packages/react-components/src/components/resource-explorers/explorers/asset-explorer/internal-asset-explorer.tsx index d4a998c26..c8f2eeb30 100644 --- a/packages/react-components/src/components/resource-explorers/explorers/asset-explorer/internal-asset-explorer.tsx +++ b/packages/react-components/src/components/resource-explorers/explorers/asset-explorer/internal-asset-explorer.tsx @@ -57,7 +57,7 @@ export function InternalAssetExplorer({ const tableResourceDefinition = customTableResourceDefinition ?? createDefaultAssetTableDefinition((asset) => { - if ((asset.hierarchies ?? []).length > 0) { + if ((asset.hierarchies ?? []).length > 0 && parameters === undefined) { return ( onClickAssetName(asset)}>{asset.name} ); diff --git a/packages/react-components/src/components/resource-explorers/explorers/asset-explorer/use-assets/use-assets.ts b/packages/react-components/src/components/resource-explorers/explorers/asset-explorer/use-assets/use-assets.ts index 2d2b450ec..abf3b27fe 100644 --- a/packages/react-components/src/components/resource-explorers/explorers/asset-explorer/use-assets/use-assets.ts +++ b/packages/react-components/src/components/resource-explorers/explorers/asset-explorer/use-assets/use-assets.ts @@ -53,7 +53,9 @@ export function useAssets({ const shouldRequestAssetModelAssets = !shouldRequestChildAssets && isEveryAssetModelAssetsParameters(parameters); const shouldRequestRootAssets = - !shouldRequestAssetModelAssets && !shouldRequestChildAssets; + !shouldRequestAssetModelAssets && + !shouldRequestChildAssets && + parameters === undefined; const assetSearchResult = useAssetSearch({ parameters: shouldRequestSearchedAssets ? parameters : [], @@ -85,7 +87,15 @@ export function useAssets({ ? assetModelAssetsQueryResult : shouldRequestChildAssets ? childAssetsQueryResult - : rootAssetsQueryResult; + : shouldRequestRootAssets + ? rootAssetsQueryResult + : { + assets: [], + isLoading: false, + error: undefined, + hasNextPage: false, + nextPage: () => {}, + }; return queryResult; } diff --git a/packages/react-components/src/components/resource-explorers/explorers/time-series-explorer/internal-time-series-explorer.tsx b/packages/react-components/src/components/resource-explorers/explorers/time-series-explorer/internal-time-series-explorer.tsx index 33ceac2a1..2c8630240 100644 --- a/packages/react-components/src/components/resource-explorers/explorers/time-series-explorer/internal-time-series-explorer.tsx +++ b/packages/react-components/src/components/resource-explorers/explorers/time-series-explorer/internal-time-series-explorer.tsx @@ -32,7 +32,7 @@ import { DEFAULT_TIME_SERIES_DROP_DOWN_DEFINITION } from '../../constants/drop-d export function InternalTimeSeriesExplorer({ requestFns, - parameters = [], + parameters = [{}], resourceName = DEFAULT_TIME_SERIES_RESOURCE_NAME, pluralResourceName = DEFAULT_PLURAL_TIME_SERIES_RESOURCE_NAME, isTimeSeriesDisabled = DEFAULT_IS_RESOURCE_DISABLED, diff --git a/packages/react-components/src/components/resource-explorers/requests/use-multiple-list-requests/types.ts b/packages/react-components/src/components/resource-explorers/requests/use-multiple-list-requests/types.ts new file mode 100644 index 000000000..c291b050c --- /dev/null +++ b/packages/react-components/src/components/resource-explorers/requests/use-multiple-list-requests/types.ts @@ -0,0 +1,9 @@ +type Parameters = readonly unknown[]; + +export type QueryKey = readonly [ + { + resourceId: string; + allParameters: Parameters; + currentParameters: Parameters[number]; + } +]; diff --git a/packages/react-components/src/components/resource-explorers/requests/use-multiple-list-requests/use-cached-resources.ts b/packages/react-components/src/components/resource-explorers/requests/use-multiple-list-requests/use-cached-resources.ts index cd5e1a66b..108fd022f 100644 --- a/packages/react-components/src/components/resource-explorers/requests/use-multiple-list-requests/use-cached-resources.ts +++ b/packages/react-components/src/components/resource-explorers/requests/use-multiple-list-requests/use-cached-resources.ts @@ -1,7 +1,10 @@ import { useQueryClient } from '@tanstack/react-query'; + +import type { QueryKey } from './types'; import { resourceExplorerQueryClient } from '../resource-explorer-query-client'; export interface UseCachedResourcesOptions { + resourceId: string; allParameters: readonly unknown[]; } @@ -9,13 +12,22 @@ export type UseCachedResourcesResult = Resource[]; /** Use resources loaded into the cache. */ export function useCachedResources({ + resourceId, allParameters, }: UseCachedResourcesOptions): UseCachedResourcesResult { const queryClient = useQueryClient(resourceExplorerQueryClient); const resourcePages = queryClient.getQueriesData({ - queryKey: [{ allParameters }], + queryKey: [{ resourceId, allParameters }], + // Further filter the data to prevent partial matching on allParameters + predicate: (query) => { + return ( + JSON.stringify((query.queryKey as QueryKey)[0].allParameters) === + JSON.stringify(allParameters) + ); + }, }); + const resources = resourcePages.flatMap(([_, resources = []]) => resources); return resources; diff --git a/packages/react-components/src/components/resource-explorers/requests/use-multiple-list-requests/use-multiple-list-requests.ts b/packages/react-components/src/components/resource-explorers/requests/use-multiple-list-requests/use-multiple-list-requests.ts index 81441d8f0..80703f2e2 100644 --- a/packages/react-components/src/components/resource-explorers/requests/use-multiple-list-requests/use-multiple-list-requests.ts +++ b/packages/react-components/src/components/resource-explorers/requests/use-multiple-list-requests/use-multiple-list-requests.ts @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { useQueryPagination } from './use-two-dimensional-pagination'; import { useCachedResources } from './use-cached-resources'; +import type { QueryKey } from './types'; import { ListRequestBaseParams, ListRequestBaseResponse, @@ -24,16 +25,6 @@ export interface UseMultipleListRequestsResult resources: Resource[]; } -type Parameters = readonly unknown[]; - -type QueryKey = [ - { - resourceId: string; - allParameters: Parameters; - currentParameters: Parameters[number]; - } -]; - /** Use paginated resources across multiple queries. */ export function useMultipleListRequests< Params extends ListRequestBaseParams, @@ -99,6 +90,7 @@ export function useMultipleListRequests< ); const resources = useCachedResources({ + resourceId, allParameters: parameters, }); diff --git a/packages/react-components/src/components/resource-explorers/testing/helpers/selectors.ts b/packages/react-components/src/components/resource-explorers/testing/helpers/selectors.ts deleted file mode 100644 index c8911ea58..000000000 --- a/packages/react-components/src/components/resource-explorers/testing/helpers/selectors.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { screen } from '@testing-library/react'; - -export function createSelectLoadingResources(pluralResourceType: string) { - return function selectLoadingResources() { - return screen.queryByText(`Loading ${pluralResourceType.toLowerCase()}...`); - }; -} - -export function selectTableSearch() { - return screen.queryByLabelText('Search'); -} - -export function selectTableFilter() { - return screen.queryByLabelText('Filter'); -} - -export function selectPreviousPageButton() { - return screen.getByRole('button', { - name: 'Previous page', - }); -} - -export function selectNextPageButton() { - return screen.getByRole('button', { name: 'Next page' }); -} diff --git a/packages/react-components/src/components/resource-explorers/testing/helpers/table.tsx b/packages/react-components/src/components/resource-explorers/testing/helpers/table.tsx index 9b3928f00..d57387fc5 100644 --- a/packages/react-components/src/components/resource-explorers/testing/helpers/table.tsx +++ b/packages/react-components/src/components/resource-explorers/testing/helpers/table.tsx @@ -62,6 +62,18 @@ export function queryColumnDisplayCheckbox(columnName: string) { return screen.queryByRole('checkbox', { name: columnName }); } +export function getSearchField() { + return screen.getByLabelText('Search'); +} + +export function querySearchField() { + return screen.queryByLabelText('Search'); +} + +export async function typeSearchStatement(searchStatement: string) { + await userEvent.type(getSearchField(), searchStatement); +} + export async function clickSearch() { await waitFor(() => { userEvent.click(screen.getByRole('button', { name: 'Search' })); @@ -72,3 +84,14 @@ export async function clickSearch() { expect(screen.queryByText(/Loading/)).not.toBeInTheDocument(); }); } + +export async function pressReturnKeyToSearch() { + await waitFor(() => { + userEvent.keyboard('{Enter}'); + expect(screen.getByText(/Loading/)).toBeVisible(); + }); + + await waitFor(() => { + expect(screen.queryByText(/Loading/)).not.toBeInTheDocument(); + }); +} diff --git a/packages/react-components/src/components/resource-explorers/testing/table-variant/asset-model-table.spec.tsx b/packages/react-components/src/components/resource-explorers/testing/table-variant/asset-model-table.spec.tsx index b49b9c278..bf1b90b15 100644 --- a/packages/react-components/src/components/resource-explorers/testing/table-variant/asset-model-table.spec.tsx +++ b/packages/react-components/src/components/resource-explorers/testing/table-variant/asset-model-table.spec.tsx @@ -47,7 +47,7 @@ describe('asset model table', () => { expect(screen.getByText('(0)')).toBeVisible(); // Search - expect(screen.queryByLabelText('Search')).not.toBeInTheDocument(); + expect(table.querySearchField()).not.toBeInTheDocument(); // Filter expect(screen.queryByLabelText('Filter')).not.toBeInTheDocument(); diff --git a/packages/react-components/src/components/resource-explorers/testing/table-variant/asset-property-table.spec.tsx b/packages/react-components/src/components/resource-explorers/testing/table-variant/asset-property-table.spec.tsx index dc7d633f9..3aa9bca9b 100644 --- a/packages/react-components/src/components/resource-explorers/testing/table-variant/asset-property-table.spec.tsx +++ b/packages/react-components/src/components/resource-explorers/testing/table-variant/asset-property-table.spec.tsx @@ -59,7 +59,7 @@ describe('asset property table', () => { expect(screen.getByText('(0)')).toBeVisible(); // Search - expect(screen.queryByLabelText('Search')).not.toBeInTheDocument(); + expect(table.querySearchField()).not.toBeInTheDocument(); // Filter expect(screen.queryByLabelText('Filter')).not.toBeInTheDocument(); @@ -90,7 +90,7 @@ describe('asset property table', () => { ); - expect(screen.getByLabelText('Search')).toBeVisible(); + expect(table.getSearchField()).toBeVisible(); }); it('renders with filter enabled', () => { @@ -364,14 +364,13 @@ describe('asset property table', () => { .mockResolvedValue({ rows: [] } satisfies Awaited< ReturnType >); - const user = userEvent.setup(); render( ); - await user.type(screen.getByLabelText('Search'), 'Asset Property'); + await table.typeSearchStatement('Asset Property'); await table.clickSearch(); const queryStatement = executeQuery.mock.calls[0][0].queryStatement; @@ -415,7 +414,6 @@ describe('asset property table', () => { const executeQuery = jest.fn().mockResolvedValue({ rows: [assetPropertyRow1, assetPropertyRow2, assetPropertyRow3], } satisfies Awaited>); - const user = userEvent.setup(); render( { /> ); - await user.type(screen.getByLabelText('Search'), 'Asset Property'); + await table.typeSearchStatement('Asset Property'); await table.clickSearch(); // Name is rendered @@ -548,6 +546,24 @@ describe('asset property table', () => { screen.queryByText(assetPropertyRow3.data[3].scalarValue) ).not.toBeInTheDocument(); }); + + it('initiates search when user presses enter/return key', async () => { + const executeQuery = jest + .fn() + .mockResolvedValue({ rows: [] } satisfies Awaited< + ReturnType + >); + render( + + ); + await table.typeSearchStatement('Asset Property'); + await table.pressReturnKeyToSearch(); + + expect(executeQuery).toHaveBeenCalledOnce(); + }); }); describe('latest values', () => { diff --git a/packages/react-components/src/components/resource-explorers/testing/table-variant/asset-table.spec.tsx b/packages/react-components/src/components/resource-explorers/testing/table-variant/asset-table.spec.tsx index aca70e188..24ac1a308 100644 --- a/packages/react-components/src/components/resource-explorers/testing/table-variant/asset-table.spec.tsx +++ b/packages/react-components/src/components/resource-explorers/testing/table-variant/asset-table.spec.tsx @@ -45,7 +45,7 @@ describe('asset table', () => { expect(screen.getByText('(0)')).toBeVisible(); // Search - expect(screen.queryByLabelText('Search')).not.toBeInTheDocument(); + expect(table.querySearchField()).not.toBeInTheDocument(); // Filter expect(screen.queryByLabelText('Filter')).not.toBeInTheDocument(); @@ -72,7 +72,7 @@ describe('asset table', () => { it('renders with search enabled', () => { render(); - expect(screen.getByLabelText('Search')).toBeVisible(); + expect(table.getSearchField()).toBeVisible(); }); it('renders with filter enabled', () => { @@ -249,14 +249,13 @@ describe('asset table', () => { .mockResolvedValue({ rows: [] } satisfies Awaited< ReturnType >); - const user = userEvent.setup(); render( ); - await user.type(screen.getByLabelText('Search'), 'Asset'); + await table.typeSearchStatement('Asset'); await table.clickSearch(); const queryStatement = executeQuery.mock.calls[0][0].queryStatement; @@ -299,7 +298,6 @@ describe('asset table', () => { const executeQuery = jest.fn().mockResolvedValue({ rows: [assetRow1, assetRow2, assetRow3], } satisfies Awaited>); - const user = userEvent.setup(); render( { /> ); - await user.type(screen.getByLabelText('Search'), 'Asset'); + await table.typeSearchStatement('Asset'); await table.clickSearch(); // Name is rendered @@ -408,6 +406,24 @@ describe('asset table', () => { screen.queryByText(assetRow3.data[3].scalarValue) ).not.toBeInTheDocument(); }); + + it('initiates search when user presses enter/return key', async () => { + const executeQuery = jest + .fn() + .mockResolvedValue({ rows: [] } satisfies Awaited< + ReturnType + >); + render( + + ); + await table.typeSearchStatement('Asset'); + await table.pressReturnKeyToSearch(); + + expect(executeQuery).toHaveBeenCalledOnce(); + }); }); describe('selection', () => { diff --git a/packages/react-components/src/components/resource-explorers/testing/table-variant/time-series-table.spec.tsx b/packages/react-components/src/components/resource-explorers/testing/table-variant/time-series-table.spec.tsx index d1742014a..cf54655d3 100644 --- a/packages/react-components/src/components/resource-explorers/testing/table-variant/time-series-table.spec.tsx +++ b/packages/react-components/src/components/resource-explorers/testing/table-variant/time-series-table.spec.tsx @@ -52,7 +52,7 @@ describe('time series table', () => { expect(screen.getByText('(0)')).toBeVisible(); // Search - expect(screen.queryByLabelText('Search')).not.toBeInTheDocument(); + expect(table.querySearchField()).not.toBeInTheDocument(); // Filter expect(screen.queryByLabelText('Filter')).not.toBeInTheDocument(); diff --git a/packages/react-components/src/components/resource-explorers/types/resource-explorer.ts b/packages/react-components/src/components/resource-explorers/types/resource-explorer.ts index 072c8d7dd..cc6828783 100644 --- a/packages/react-components/src/components/resource-explorers/types/resource-explorer.ts +++ b/packages/react-components/src/components/resource-explorers/types/resource-explorer.ts @@ -17,7 +17,7 @@ import { } from './drop-down'; /** Props common to all resource explorers. */ -export type CommonResourceExplorerProps = { +export type CommonResourceExplorerProps = { /** TODO */ requestFns?: unknown; diff --git a/packages/react-components/src/components/resource-explorers/variants/resource-table/resource-table-search.tsx b/packages/react-components/src/components/resource-explorers/variants/resource-table/resource-table-search.tsx index 41404370c..eb0d36c9e 100644 --- a/packages/react-components/src/components/resource-explorers/variants/resource-table/resource-table-search.tsx +++ b/packages/react-components/src/components/resource-explorers/variants/resource-table/resource-table-search.tsx @@ -20,6 +20,12 @@ export function ResourceTableSearch({ }: ResourceTableSearchProps) { const [searchInputValue, setSearchInputValue] = useState(''); + function handleKeyDown(key: string) { + if (key === 'Enter') { + onClickSearch(searchInputValue); + } + } + function handleClickSearch() { onClickSearch(searchInputValue); } @@ -49,6 +55,7 @@ export function ResourceTableSearch({ onChange={({ detail: { value } }) => setSearchInputValue(value)} placeholder='Search for resources' controlId='search' + onKeyDown={({ detail: { key } }) => handleKeyDown(key)} /> diff --git a/packages/react-components/stories/resource-explorers/asset-explorer.stories.tsx b/packages/react-components/stories/resource-explorers/asset-explorer.stories.tsx index 070e9a7bf..2b2450c8a 100644 --- a/packages/react-components/stories/resource-explorers/asset-explorer.stories.tsx +++ b/packages/react-components/stories/resource-explorers/asset-explorer.stories.tsx @@ -1,107 +1,101 @@ import { type Meta } from '@storybook/react'; -import React, { useState } from 'react'; +import React from 'react'; -import { SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES } from './constants'; +import { + CommonResourceExplorerControls, + SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES, +} from './controls'; import { client } from './data-source'; import { AssetExplorer, type AssetExplorerProps, } from '../../src/components/resource-explorers'; -import type { ResourceExplorerStoryControls } from './types'; -import type { AssetResource } from '../../src/components/resource-explorers/types/resources'; +import { + StoryWithClearedResourceCache, + StoryWithSelectableResource, + StoryWithTanstackDevTools, +} from './decorators'; +import { StoryFnReactReturnType } from '@storybook/react/dist/ts3.9/client/preview/types'; export default { title: 'Resource Explorers/Asset Explorer', component: AssetExplorer, + parameters: { + controls: { + expanded: true, + }, + }, + decorators: [ + StoryWithTanstackDevTools, + StoryWithClearedResourceCache, + StoryWithSelectableResource, + ], argTypes: { ...SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES, }, } satisfies Meta; -type AssetExplorerStoryControls = ResourceExplorerStoryControls; +type AssetExplorerStory = ( + controls: AssetExplorerStoryControls, + context: AssetExplorerStoryContext +) => StoryFnReactReturnType; -export function StandardExample({ - isTitleEnabled, - isSearchEnabled, - isFilterEnabled, - isUserSettingsEnabled, - ...assetExplorerProps -}: AssetExplorerStoryControls) { - const [selectedAssets, setSelectedAssets] = useState< - NonNullable - >([]); +type AssetExplorerStoryControls = CommonResourceExplorerControls; - return ( - - ); +interface AssetExplorerStoryContext { + selectedResources: NonNullable; + onSelectResource: NonNullable; } -export function SearchOnly() { - return ( - - ); +function storyArgsToProps( + { + isTableTitleEnabled, + isTableSearchEnabled, + isTableFilterEnabled, + isTableUserSettingsEnabled, + isDropDownFilterEnabled, + ...controls + }: AssetExplorerStoryControls, + { selectedResources, onSelectResource }: AssetExplorerStoryContext +): AssetExplorerProps { + return { + selectedAssets: selectedResources, + onSelectAsset: onSelectResource, + tableSettings: { + isTitleEnabled: isTableTitleEnabled, + isSearchEnabled: isTableSearchEnabled, + isFilterEnabled: isTableFilterEnabled, + isUserSettingsEnabled: isTableUserSettingsEnabled, + }, + dropDownSettings: { + isFilterEnabled: isDropDownFilterEnabled, + }, + ...controls, + }; } -export function ZeroConfiguration() { - return ; -} +export const HierarchyNavigation: AssetExplorerStory = (controls, context) => { + const props = storyArgsToProps(controls, context); -export function ZeroConfigurationDropDown() { - return ; -} + return ; +}; -export function GrafanaThemeTable() { - const [selectedAssets, setSelectedAssets] = useState< - NonNullable - >([]); +export const SearchOnly: AssetExplorerStory = (controls, context) => { + const props = storyArgsToProps(controls, context); return ( ); -} +}; -export function GrafanaThemeDropDown() { - const [selectedAssets, setSelectedAssets] = useState< - NonNullable - >([]); +export const ZeroConfigurationTable: AssetExplorerStory = () => { + return ; +}; - return ( - - ); -} +export const ZeroConfigurationDropDown: AssetExplorerStory = () => { + return ; +}; diff --git a/packages/react-components/stories/resource-explorers/asset-model-explorer.stories.tsx b/packages/react-components/stories/resource-explorers/asset-model-explorer.stories.tsx index 855235b77..b2e99aac7 100644 --- a/packages/react-components/stories/resource-explorers/asset-model-explorer.stories.tsx +++ b/packages/react-components/stories/resource-explorers/asset-model-explorer.stories.tsx @@ -1,76 +1,123 @@ import { type Meta } from '@storybook/react'; -import React, { useState } from 'react'; +import React from 'react'; -import { SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES } from './constants'; +import { + SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES, + type CommonResourceExplorerControls, +} from './controls'; import { client } from './data-source'; -import type { ResourceExplorerStoryControls } from './types'; -import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { AssetModelExplorer, type AssetModelExplorerProps, } from '../../src/components/resource-explorers'; -import type { AssetModelResource } from '../../src/components/resource-explorers/types/resources'; -import { resourceExplorerQueryClient } from '../../src/components/resource-explorers/requests/resource-explorer-query-client'; +import { + StoryWithClearedResourceCache, + StoryWithSelectableResource, + StoryWithTanstackDevTools, +} from './decorators'; +import { StoryFnReactReturnType } from '@storybook/react/dist/ts3.9/client/preview/types'; export default { title: 'Resource Explorers/Asset Model Explorer', component: AssetModelExplorer, + parameters: { + controls: { + expanded: true, + exclude: ['tableSettings.isSearchEnabled'], + }, + }, + decorators: [ + StoryWithTanstackDevTools, + StoryWithClearedResourceCache, + StoryWithSelectableResource, + ], argTypes: { ...SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES, - assetModelTypes: { - type: 'radio', - default: undefined, - options: [undefined, 'ASSET_MODEL', 'COMPONENT_MODEL'], - mapping: { - undefined: [undefined], - ASSET_MODEL: ['ASSET_MODEL'], - COMPONENT_MODEL: ['COMPONENT_MODEL'], - }, - }, }, } satisfies Meta; -type AssetModelExplorerStoryControls = - ResourceExplorerStoryControls & - Pick; +type AssetModelExplorerStory = ( + controls: AssetModelExplorerStoryControls, + context: AssetModelExplorerStoryContext +) => StoryFnReactReturnType; + +type AssetModelExplorerStoryControls = CommonResourceExplorerControls; -export function StandardExample({ - isTitleEnabled, - isFilterEnabled, - isUserSettingsEnabled, - ...assetModelExplorerProps -}: AssetModelExplorerStoryControls) { - const [selectedAssetModels, setSelectedAssetModels] = useState< - NonNullable - >([]); +interface AssetModelExplorerStoryContext { + selectedResources: NonNullable< + AssetModelExplorerProps['selectedAssetModels'] + >; + onSelectResource: NonNullable; +} + +function storyArgsToProps( + { + isTableTitleEnabled, + isTableSearchEnabled, + isTableFilterEnabled, + isTableUserSettingsEnabled, + isDropDownFilterEnabled, + ...controls + }: AssetModelExplorerStoryControls, + { selectedResources, onSelectResource }: AssetModelExplorerStoryContext +): AssetModelExplorerProps { + return { + selectedAssetModels: selectedResources, + onSelectAssetModel: onSelectResource, + tableSettings: { + isTitleEnabled: isTableTitleEnabled, + isSearchEnabled: isTableSearchEnabled, + isFilterEnabled: isTableFilterEnabled, + isUserSettingsEnabled: isTableUserSettingsEnabled, + }, + dropDownSettings: { + isFilterEnabled: isDropDownFilterEnabled, + }, + ...controls, + }; +} + +export const AssetAndComponentModels: AssetModelExplorerStory = ( + controls, + context +) => { + const props = storyArgsToProps(controls, context); + + return ; +}; + +export const OnlyAssetModels: AssetModelExplorerStory = (controls, context) => { + const props = storyArgsToProps(controls, context); return ( - <> - - - + ); -} +}; + +export const OnlyComponentModels: AssetModelExplorerStory = ( + controls, + context +) => { + const props = storyArgsToProps(controls, context); -export function ZeroConfigurationTable() { + return ( + + ); +}; + +export const ZeroConfigurationTable: AssetModelExplorerStory = () => { return ; -} +}; -export function ZeroConfigurationDropDown() { +export const ZeroConfigurationDropDown: AssetModelExplorerStory = () => { return ; -} +}; diff --git a/packages/react-components/stories/resource-explorers/asset-property-explorer.stories.tsx b/packages/react-components/stories/resource-explorers/asset-property-explorer.stories.tsx index bd18006d3..d59704a1d 100644 --- a/packages/react-components/stories/resource-explorers/asset-property-explorer.stories.tsx +++ b/packages/react-components/stories/resource-explorers/asset-property-explorer.stories.tsx @@ -1,78 +1,117 @@ import { type Meta } from '@storybook/react'; -import React, { useState } from 'react'; -import { SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES } from './constants'; +import React from 'react'; +import { + CommonResourceExplorerControls, + SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES, +} from './controls'; import { client } from './data-source'; import { AssetPropertyExplorer, type AssetPropertyExplorerProps, } from '../../src/components/resource-explorers'; -import { ResourceExplorerStoryControls } from './types'; -import type { AssetPropertyResource } from '../../src/components/resource-explorers/types/resources'; +import { StoryFnReactReturnType } from '@storybook/react/dist/ts3.9/client/preview/types'; +import { + StoryWithClearedResourceCache, + StoryWithSelectableResource, + StoryWithTanstackDevTools, +} from './decorators'; export default { title: 'Resource Explorers/Asset Property Explorer', component: AssetPropertyExplorer, + parameters: { + controls: { + expanded: true, + }, + }, + decorators: [ + StoryWithTanstackDevTools, + StoryWithClearedResourceCache, + StoryWithSelectableResource, + ], argTypes: { ...SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES, }, } satisfies Meta; -type AssetPropertyExplorerStoryControls = - ResourceExplorerStoryControls; +type AssetPropertyExplorerStory = ( + controls: AssetPropertyExplorerStoryControls, + context: AssetPropertyExplorerStoryContext +) => StoryFnReactReturnType; + +type AssetPropertyExplorerStoryControls = CommonResourceExplorerControls; + +interface AssetPropertyExplorerStoryContext { + selectedResources: NonNullable< + AssetPropertyExplorerProps['selectedAssetProperties'] + >; + onSelectResource: NonNullable< + AssetPropertyExplorerProps['onSelectAssetProperty'] + >; +} + +function storyArgsToProps( + { + isTableTitleEnabled, + isTableSearchEnabled, + isTableFilterEnabled, + isTableUserSettingsEnabled, + isDropDownFilterEnabled, + ...controls + }: AssetPropertyExplorerStoryControls, + { selectedResources, onSelectResource }: AssetPropertyExplorerStoryContext +): AssetPropertyExplorerProps { + return { + selectedAssetProperties: selectedResources, + onSelectAssetProperty: onSelectResource, + tableSettings: { + isTitleEnabled: isTableTitleEnabled, + isSearchEnabled: isTableSearchEnabled, + isFilterEnabled: isTableFilterEnabled, + isUserSettingsEnabled: isTableUserSettingsEnabled, + }, + dropDownSettings: { + isFilterEnabled: isDropDownFilterEnabled, + }, + ...controls, + }; +} -export function AssetPropertyExplorerExample({ - isTitleEnabled, - isSearchEnabled, - isFilterEnabled, - isUserSettingsEnabled, - ...assetPropertyExplorerProps -}: AssetPropertyExplorerStoryControls) { - const [selectedAssetProperties, setSelectedAssetProperties] = useState< - NonNullable - >([]); +// Requires setting parameters manually (or using search) +export const WithLatestValues: AssetPropertyExplorerStory = ( + controls, + context +) => { + const props = storyArgsToProps(controls, context); + + return ( + + ); +}; + +// Requires setting parameters manually (or using search) +export const WithoutLatestValues: AssetPropertyExplorerStory = ( + controls, + context +) => { + const props = storyArgsToProps(controls, context); return ( ); -} +}; -export function ZeroConfiguration() { +export const ZeroConfigurationTable: AssetPropertyExplorerStory = () => { return ; -} +}; + +export const ZeroConfigurationDropDown: AssetPropertyExplorerStory = () => { + return ; +}; diff --git a/packages/react-components/stories/resource-explorers/constants.ts b/packages/react-components/stories/resource-explorers/constants.ts deleted file mode 100644 index c45ed5c75..000000000 --- a/packages/react-components/stories/resource-explorers/constants.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Meta } from '@storybook/react'; -import type { ResourceExplorerStoryControls } from './types'; - -export const SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES = { - selectionMode: { - control: { type: 'radio' }, - options: [undefined, 'single', 'multi'], - defaultValue: undefined, - }, - - isFilterEnabled: { - control: { type: 'boolean' }, - defaultValue: false, - }, - - isUserSettingsEnabled: { - control: { type: 'boolean' }, - defaultValue: false, - }, - - isSearchEnabled: { - control: { type: 'boolean' }, - defaultValue: false, - }, - - isTitleEnabled: { control: { type: 'boolean' }, defaultValue: true }, - - shouldPersistUserCustomization: { - control: { type: 'boolean' }, - defaultValue: false, - }, - - defaultPageSize: { - control: { type: 'radio' }, - options: [10, 25, 100, 250], - defaultValue: 10, - }, - - variant: { - control: { type: 'radio' }, - options: ['table', 'drop-down'], - defaultValue: 'table', - }, -} satisfies Meta>['argTypes']; diff --git a/packages/react-components/stories/resource-explorers/controls.ts b/packages/react-components/stories/resource-explorers/controls.ts new file mode 100644 index 000000000..f4c258619 --- /dev/null +++ b/packages/react-components/stories/resource-explorers/controls.ts @@ -0,0 +1,78 @@ +import type { Meta } from '@storybook/react'; +import type { + IsTableFilterEnabled, + IsTableSearchEnabled, + IsTableUserSettingsEnabled, + IsTitleEnabled, + PageSize, + ResourceExplorerVariant, + SelectionMode, + ShouldPersistUserCustomization, +} from '../../src/components/resource-explorers/types/common'; +import { IsDropDownFilterEnabled } from '../../src/components/resource-explorers/types/drop-down'; + +export interface CommonResourceExplorerControls { + selectionMode: SelectionMode; + shouldPersistUserCustomization: ShouldPersistUserCustomization; + defaultPageSize: PageSize; + variant: ResourceExplorerVariant; + isTableTitleEnabled: IsTitleEnabled; + isTableSearchEnabled: IsTableSearchEnabled; + isTableFilterEnabled: IsTableFilterEnabled; + isTableUserSettingsEnabled: IsTableUserSettingsEnabled; + isDropDownFilterEnabled: IsDropDownFilterEnabled; +} + +export const SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES = { + selectionMode: { + control: { type: 'radio' }, + options: [undefined, 'single', 'multi'], + defaultValue: undefined, + }, + defaultPageSize: { + control: { type: 'radio' }, + options: [10, 25, 100, 250], + defaultValue: 10, + }, + shouldPersistUserCustomization: { + control: { type: 'boolean' }, + defaultValue: false, + description: 'Specify if user settings should be stored by the browser.', + }, + variant: { + control: { type: 'radio' }, + options: ['table', 'drop-down'], + defaultValue: 'table', + }, + isTableTitleEnabled: { + name: 'tableSettings.isTitleEnabled', + control: { type: 'boolean' }, + defaultValue: true, + if: { arg: 'variant', eq: 'table' }, + }, + isTableSearchEnabled: { + name: 'tableSettings.isSearchEnabled', + control: { type: 'boolean' }, + defaultValue: false, + if: { arg: 'variant', eq: 'table' }, + }, + isTableFilterEnabled: { + name: 'tableSettings.isFilterEnabled', + control: { type: 'boolean' }, + defaultValue: false, + if: { arg: 'variant', eq: 'table' }, + }, + isTableUserSettingsEnabled: { + name: 'tableSettings.isUserSettingsEnabled', + control: { type: 'boolean' }, + defaultValue: false, + if: { arg: 'variant', eq: 'table' }, + }, + isDropDownFilterEnabled: { + name: 'dropDownSettings.isFilterEnabled', + control: { type: 'boolean' }, + defaultValue: false, + description: 'Enable the drop-drop down filter', + if: { arg: 'variant', eq: 'drop-down' }, + }, +} satisfies Meta['argTypes']; diff --git a/packages/react-components/stories/resource-explorers/decorators.tsx b/packages/react-components/stories/resource-explorers/decorators.tsx new file mode 100644 index 000000000..7f0cb11ec --- /dev/null +++ b/packages/react-components/stories/resource-explorers/decorators.tsx @@ -0,0 +1,32 @@ +import { type DecoratorFn } from '@storybook/react'; +import React, { useEffect, useState } from 'react'; +import { resourceExplorerQueryClient } from '../../src/components/resource-explorers/requests'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; + +export const StoryWithTanstackDevTools: DecoratorFn = (Story) => { + return ( + <> + + + + ); +}; + +export const StoryWithClearedResourceCache: DecoratorFn = (Story) => { + useEffect(() => { + resourceExplorerQueryClient.clear(); + }, []); + + return ; +}; + +export const StoryWithSelectableResource: DecoratorFn = (Story) => { + const [selectedResources, setSelectedResources] = useState([]); + + return ( + + ); +}; diff --git a/packages/react-components/stories/resource-explorers/explorer-combinations.stories.tsx b/packages/react-components/stories/resource-explorers/explorer-combinations.stories.tsx index b9f858d3d..e1620a39f 100644 --- a/packages/react-components/stories/resource-explorers/explorer-combinations.stories.tsx +++ b/packages/react-components/stories/resource-explorers/explorer-combinations.stories.tsx @@ -1,32 +1,48 @@ import { type Meta } from '@storybook/react'; import React, { useState } from 'react'; -import { SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES } from './constants'; +import { + StoryWithClearedResourceCache, + StoryWithTanstackDevTools, +} from './decorators'; import { client } from './data-source'; - import { AssetModelExplorer, AssetExplorer, AssetPropertyExplorer, + TimeSeriesExplorer, type AssetModelExplorerProps, type AssetExplorerProps, - type AssetPropertyExplorerProps, } from '../../src/components/resource-explorers'; export default { title: 'Resource Explorers/Combinations', component: AssetExplorer, - argTypes: { - ...SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES, - }, + decorators: [StoryWithTanstackDevTools, StoryWithClearedResourceCache], } satisfies Meta; -export function AssetPlusAssetPropertyExplorer() { +export function AssetExplorerPlusAssetPropertyExplorer() { const [selectedAssets, setSelectedAssets] = useState< NonNullable >([]); - const [selectedAssetProperties, setSelectedAssetProperties] = useState< - NonNullable + + return ( + <> + + + + + ); +} + +export function AssetExplorerPlusTimeSeriesExplorer() { + const [selectedAssets, setSelectedAssets] = useState< + NonNullable >([]); return ( @@ -36,20 +52,78 @@ export function AssetPlusAssetPropertyExplorer() { onSelectAsset={setSelectedAssets} selectedAssets={selectedAssets} selectionMode='multi' + tableSettings={{ + isSearchEnabled: true, + isFilterEnabled: true, + isUserSettingsEnabled: true, + }} /> - + + ); +} + +export function AssetExplorerPlusAssetExplorer() { + const [selectedAssets, setSelectedAssets] = useState< + NonNullable + >([]); + + return ( + <> + + + + + ); +} + +export function AssetModelExplorerPlusAssetExplorer() { + const [selectedAssetModels, setSelectedAssetModels] = useState< + NonNullable + >([]); + + return ( + <> + + + ); } -export function AssetModelPlusAssetExplorer() { +export function AssetModelExplorerPlusAssetExplorerPlusAssetPropertyExplorer() { const [selectedAssetModels, setSelectedAssetModels] = useState< NonNullable >([]); @@ -60,19 +134,80 @@ export function AssetModelPlusAssetExplorer() { return ( <> + + + + ); +} + +export function AssetModelExplorerPlusAssetExplorerPlusTimeSeriesExplorer() { + const [selectedAssetModels, setSelectedAssetModels] = useState< + NonNullable + >([]); + const [selectedAssets, setSelectedAssets] = useState< + NonNullable + >([]); + + return ( + <> + + + + + ); diff --git a/packages/react-components/stories/resource-explorers/time-series-explorer.stories.tsx b/packages/react-components/stories/resource-explorers/time-series-explorer.stories.tsx index 02dbfeb15..88d0350e7 100644 --- a/packages/react-components/stories/resource-explorers/time-series-explorer.stories.tsx +++ b/packages/react-components/stories/resource-explorers/time-series-explorer.stories.tsx @@ -1,106 +1,138 @@ import { type Meta } from '@storybook/react'; -import React, { useState } from 'react'; +import React from 'react'; -import { SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES } from './constants'; +import { + CommonResourceExplorerControls, + SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES, +} from './controls'; import { client } from './data-source'; import { TimeSeriesExplorer, type TimeSeriesExplorerProps, } from '../../src/components/resource-explorers'; -import type { ResourceExplorerStoryControls } from './types'; -import type { TimeSeriesResource } from '../../src/components/resource-explorers/types/resources'; +import { StoryFnReactReturnType } from '@storybook/react/dist/ts3.9/client/preview/types'; +import { + StoryWithClearedResourceCache, + StoryWithSelectableResource, + StoryWithTanstackDevTools, +} from './decorators'; export default { title: 'Resource Explorers/Time Series Explorer', component: TimeSeriesExplorer, + parameters: { + controls: { + expanded: true, + exclude: ['tableSettings.isSearchEnabled'], + }, + }, + decorators: [ + StoryWithTanstackDevTools, + StoryWithClearedResourceCache, + StoryWithSelectableResource, + ], argTypes: { ...SHARED_RESOURCE_EXPLORER_STORY_ARG_TYPES, }, } satisfies Meta; -type TimeSeriesExplorerStoryControls = - ResourceExplorerStoryControls & - Pick; +type TimeSeriesExplorerStory = ( + controls: TimeSeriesExplorerStoryControls, + context: AssetPropertyExplorerStoryContext +) => StoryFnReactReturnType; + +type TimeSeriesExplorerStoryControls = CommonResourceExplorerControls; + +interface AssetPropertyExplorerStoryContext { + selectedResources: NonNullable; + onSelectResource: NonNullable; +} + +function storyArgsToProps( + { + isTableTitleEnabled, + isTableSearchEnabled, + isTableFilterEnabled, + isTableUserSettingsEnabled, + isDropDownFilterEnabled, + ...controls + }: TimeSeriesExplorerStoryControls, + { selectedResources, onSelectResource }: AssetPropertyExplorerStoryContext +): TimeSeriesExplorerProps { + return { + selectedTimeSeries: selectedResources, + onSelectTimeSeries: onSelectResource, + tableSettings: { + isTitleEnabled: isTableTitleEnabled, + isSearchEnabled: isTableSearchEnabled, + isFilterEnabled: isTableFilterEnabled, + isUserSettingsEnabled: isTableUserSettingsEnabled, + }, + dropDownSettings: { + isFilterEnabled: isDropDownFilterEnabled, + }, + ...controls, + }; +} + +export const AllTimeSeriesWithLatestValues: TimeSeriesExplorerStory = ( + controls, + context +) => { + const props = storyArgsToProps(controls, context); + + return ; +}; -export function AllTimeSeries({ - isTitleEnabled, - isFilterEnabled, - isUserSettingsEnabled, - ...timeSeriesExplorerProps -}: TimeSeriesExplorerStoryControls) { - const [selectedTimeSeries, setSeletedTimeSeries] = useState< - NonNullable - >([]); +export const AllTimeSeriesWithoutLatestValues: TimeSeriesExplorerStory = ( + controls, + context +) => { + const props = storyArgsToProps(controls, context); return ( ); -} +}; -export function AllAssociatedTimeSeries({ - isTitleEnabled, - isFilterEnabled, - isUserSettingsEnabled, - ...timeSeriesExplorerProps -}: TimeSeriesExplorerStoryControls) { - const [selectedTimeSeries, setSeletedTimeSeries] = useState< - NonNullable - >([]); +export const AllAssociatedTimeSeries: TimeSeriesExplorerStory = ( + controls, + context +) => { + const props = storyArgsToProps(controls, context); return ( ); -} +}; -export function AllDisassociatedTimeSeries({ - isTitleEnabled, - isFilterEnabled, - isUserSettingsEnabled, - ...timeSeriesExplorerProps -}: TimeSeriesExplorerStoryControls) { - const [selectedTimeSeries, setSeletedTimeSeries] = useState< - NonNullable - >([]); +export const AllDisassociatedTimeSeries: TimeSeriesExplorerStory = ( + controls, + context +) => { + const props = storyArgsToProps(controls, context); return ( ); -} +}; -export function ZeroConfiguration() { +export const ZeroConfigurationTable: TimeSeriesExplorerStory = () => { return ; -} +}; + +export const ZeroConfigurationDropDown: TimeSeriesExplorerStory = () => { + return ; +}; diff --git a/packages/react-components/stories/resource-explorers/types.ts b/packages/react-components/stories/resource-explorers/types.ts deleted file mode 100644 index 98a647a87..000000000 --- a/packages/react-components/stories/resource-explorers/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { CommonResourceExplorerProps } from '../../src/components/resource-explorers/types/resource-explorer'; - -export type ResourceExplorerStoryControls = Pick< - CommonResourceExplorerProps, - | 'variant' - | 'defaultPageSize' - | 'shouldPersistUserCustomization' - | 'selectionMode' -> & - CommonResourceExplorerProps['tableSettings'];