From 77b780983d2e2cf23145452961d411f18c5b6380 Mon Sep 17 00:00:00 2001 From: Tracy French Date: Fri, 24 May 2024 16:39:57 -0600 Subject: [PATCH 1/2] 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']; From 443c847a9bb4dd03768732a2578135adc4d691ba Mon Sep 17 00:00:00 2001 From: "chandrashekhara.n" Date: Tue, 21 May 2024 15:00:47 +0530 Subject: [PATCH 2/2] feat: updated the date time to support timezone #2663 --- package-lock.json | 15 +++++++ .../tests/trendCursors/trendCursors.spec.ts | 13 +++++- .../assetTableColumnDefinitionsFactory.tsx | 6 +-- ...ateModeledDataStreamColumnDefinitions.tsx} | 14 ++++-- .../unmodeledDataStreamTable.tsx | 17 ++++--- packages/react-components/package.json | 1 + .../chart/chartOptions/tooltip/time.tsx | 4 +- .../table/columnDefinitions/factory.tsx | 2 +- .../trendCursor/trendCursorHeader.tsx | 11 +++-- .../chart/legend/table/table.spec.tsx | 7 ++- .../src/components/kpi/kpi.spec.tsx | 4 +- .../src/components/kpi/kpiBase.spec.tsx | 4 +- .../src/components/kpi/kpiBase.tsx | 3 +- .../src/components/timeZone/index.ts | 1 + .../src/components/timeZone/timeZone.tsx | 44 +++++++++++++++++++ .../src/components/timestampBar/index.tsx | 11 +++-- .../trendCursors/view/utils/dateTime.ts | 10 +++++ .../trendCursors/view/utils/index.ts | 1 + .../extensions/trendCursors/view/view.ts | 12 +++-- packages/react-components/src/index.ts | 1 + .../useSiteWiseAnomalyDataSource/constants.ts | 2 +- packages/react-components/src/utils/time.ts | 1 + 22 files changed, 149 insertions(+), 35 deletions(-) rename packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/modeledDataStreamQueryEditor/modeledDataStreamExplorer/modeledDataStreamTable/{createModeledDataStreamColumnDefinitions.ts => createModeledDataStreamColumnDefinitions.tsx} (83%) create mode 100644 packages/react-components/src/components/timeZone/index.ts create mode 100644 packages/react-components/src/components/timeZone/timeZone.tsx create mode 100644 packages/react-components/src/echarts/extensions/trendCursors/view/utils/dateTime.ts diff --git a/package-lock.json b/package-lock.json index 67514741b..9e94e0ca2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40372,6 +40372,14 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/date-fns-tz": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.1.tgz", + "integrity": "sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA==", + "peerDependencies": { + "date-fns": "2.x" + } + }, "node_modules/debounce": { "version": "1.2.1", "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", @@ -73072,6 +73080,7 @@ "d3-format": "^3.1.0", "d3-shape": "^3.2.0", "date-fns": "^3.4.0", + "date-fns-tz": "^2.0.1", "dompurify": "3.0.5", "echarts": "^5.4.3", "is-hotkey": "^0.2.0", @@ -89986,6 +89995,7 @@ "d3-format": "^3.1.0", "d3-shape": "^3.2.0", "date-fns": "^3.4.0", + "date-fns-tz": "^2.0.1", "dompurify": "3.0.5", "echarts": "^5.4.3", "eslint-config-iot-app-kit": "10.5.0", @@ -113204,6 +113214,11 @@ "version": "2.29.2", "integrity": "sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA==" }, + "date-fns-tz": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.1.tgz", + "integrity": "sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA==" + }, "debounce": { "version": "1.2.1", "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" diff --git a/packages/dashboard/e2e/tests/trendCursors/trendCursors.spec.ts b/packages/dashboard/e2e/tests/trendCursors/trendCursors.spec.ts index 3cb8b2cd5..c6f26773e 100644 --- a/packages/dashboard/e2e/tests/trendCursors/trendCursors.spec.ts +++ b/packages/dashboard/e2e/tests/trendCursors/trendCursors.spec.ts @@ -1,4 +1,5 @@ import { test, expect, Page, Locator } from '@playwright/test'; +import { parse } from 'date-fns'; import { ADD_TREND_CURSOR, ASSET_MODEL_TAB, @@ -185,8 +186,16 @@ test('can drag a trend cursor', async ({ page }) => { expect(initialTrendCursorDateString).not.toEqual( updatedTrendCursorDateString ); - const initialDate = new Date(initialTrendCursorDateString); - const updatedDate = new Date(updatedTrendCursorDateString); + const initialDate = parse( + `${initialTrendCursorDateString.split('\n').join(' ')}`, + 'dd/MM/yyyy HH:mm:ss', + new Date() + ); + const updatedDate = parse( + `${updatedTrendCursorDateString.split('\n').join(' ')}`, + 'dd/MM/yyyy HH:mm:ss', + new Date() + ); expect(initialDate.getTime()).toBeGreaterThan(updatedDate.getTime()); // trend cursor should have some value diff --git a/packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/modeledDataStreamQueryEditor/assetExplorer/assetTable/assetTableColumnDefinitionsFactory.tsx b/packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/modeledDataStreamQueryEditor/assetExplorer/assetTable/assetTableColumnDefinitionsFactory.tsx index bf8de815d..0bcc182d9 100644 --- a/packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/modeledDataStreamQueryEditor/assetExplorer/assetTable/assetTableColumnDefinitionsFactory.tsx +++ b/packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/modeledDataStreamQueryEditor/assetExplorer/assetTable/assetTableColumnDefinitionsFactory.tsx @@ -2,8 +2,8 @@ import { AssetSummary } from '@aws-sdk/client-iotsitewise'; import { type TableProps } from '@cloudscape-design/components/table'; import React from 'react'; +import { DateTime } from '@iot-app-kit/react-components'; import type { AssetTableNameLinkProps } from './assetTableNameLink'; -import { getFormattedDateTime } from '~/components/util/dateTimeUtil'; type AssetTableColumnDefinitions = TableProps['columnDefinitions']; @@ -87,7 +87,7 @@ export class AssetTableColumnDefinitionsFactory { id: 'creationDate', header: 'Creation Date', cell: ({ creationDate }) => - creationDate ? getFormattedDateTime(creationDate) : '-', + creationDate ? : '-', sortingField: 'creationDate', }; } @@ -97,7 +97,7 @@ export class AssetTableColumnDefinitionsFactory { id: 'lastUpdateDate', header: 'Last Update Date', cell: ({ lastUpdateDate }) => - lastUpdateDate ? getFormattedDateTime(lastUpdateDate) : '-', + lastUpdateDate ? : '-', sortingField: 'lastUpdateDate', }; } diff --git a/packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/modeledDataStreamQueryEditor/modeledDataStreamExplorer/modeledDataStreamTable/createModeledDataStreamColumnDefinitions.ts b/packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/modeledDataStreamQueryEditor/modeledDataStreamExplorer/modeledDataStreamTable/createModeledDataStreamColumnDefinitions.tsx similarity index 83% rename from packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/modeledDataStreamQueryEditor/modeledDataStreamExplorer/modeledDataStreamTable/createModeledDataStreamColumnDefinitions.ts rename to packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/modeledDataStreamQueryEditor/modeledDataStreamExplorer/modeledDataStreamTable/createModeledDataStreamColumnDefinitions.tsx index 7081413b3..e74f1afa8 100644 --- a/packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/modeledDataStreamQueryEditor/modeledDataStreamExplorer/modeledDataStreamTable/createModeledDataStreamColumnDefinitions.ts +++ b/packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/modeledDataStreamQueryEditor/modeledDataStreamExplorer/modeledDataStreamTable/createModeledDataStreamColumnDefinitions.tsx @@ -1,7 +1,8 @@ +import React from 'react'; import { isNumeric, round } from '@iot-app-kit/core-util'; +import { DateTime } from '@iot-app-kit/react-components'; import { type TableProps } from '@cloudscape-design/components/table'; import type { ModeledDataStream } from '../types'; -import { getFormattedDateTimeFromEpoch } from '~/components/util/dateTimeUtil'; export function createModeledDataStreamColumnDefinitions( significantDigits: number @@ -33,12 +34,17 @@ export function createModeledDataStreamColumnDefinitions( id: 'latestValueTime', header: 'Latest value time', cell: ({ latestValueTime }) => { + if (!latestValueTime) return '-'; if (latestValueTime && isNumeric(latestValueTime)) { - return getFormattedDateTimeFromEpoch( - Number(round(latestValueTime, significantDigits)) + return ( + ); } - return getFormattedDateTimeFromEpoch(latestValueTime); + return ; }, sortingField: 'latestValueTime', }, diff --git a/packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/unmodeledDataStreamExplorer/unmodeledDataStreamTable/unmodeledDataStreamTable.tsx b/packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/unmodeledDataStreamExplorer/unmodeledDataStreamTable/unmodeledDataStreamTable.tsx index 36c21f018..35ca9c735 100644 --- a/packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/unmodeledDataStreamExplorer/unmodeledDataStreamTable/unmodeledDataStreamTable.tsx +++ b/packages/dashboard/src/components/queryEditor/iotSiteWiseQueryEditor/unmodeledDataStreamExplorer/unmodeledDataStreamTable/unmodeledDataStreamTable.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { type IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise'; import { isNumeric, round } from '@iot-app-kit/core-util'; +import { getPlugin } from '@iot-app-kit/core'; +import { DateTime } from '@iot-app-kit/react-components'; import { useCollection } from '@cloudscape-design/collection-hooks'; import Box from '@cloudscape-design/components/box'; @@ -16,10 +18,8 @@ import { useExplorerPreferences } from '../../useExplorerPreferences'; import { SUPPORTED_PAGE_SIZES } from '../../constants'; import { useLatestValues } from '../../useLatestValues'; import { DashboardState } from '~/store/state'; -import { getFormattedDateTimeFromEpoch } from '~/components/util/dateTimeUtil'; -import { ResourceExplorerFooter } from '../../footer/footer'; -import { getPlugin } from '@iot-app-kit/core'; import { disableAdd } from '~/components/queryEditor/iotSiteWiseQueryEditor/footer/disableAdd'; +import { ResourceExplorerFooter } from '../../footer/footer'; export interface UnmodeledDataStreamTableProps { onClickAdd: (unmodeledDataStreams: UnmodeledDataStream[]) => void; @@ -162,12 +162,17 @@ export function UnmodeledDataStreamTable({ id: 'latestValueTime', header: 'Latest value time', cell: ({ latestValueTime }) => { + if (!latestValueTime) return '-'; if (latestValueTime && isNumeric(latestValueTime)) { - return getFormattedDateTimeFromEpoch( - Number(round(latestValueTime, significantDigits)) + return ( + ); } - return getFormattedDateTimeFromEpoch(latestValueTime); + return ; }, sortingField: 'latestValueTime', }, diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 583d4bf67..e7e2735db 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -125,6 +125,7 @@ "d3-format": "^3.1.0", "d3-shape": "^3.2.0", "date-fns": "^3.4.0", + "date-fns-tz": "^2.0.1", "dompurify": "3.0.5", "echarts": "^5.4.3", "is-hotkey": "^0.2.0", diff --git a/packages/react-components/src/components/chart/chartOptions/tooltip/time.tsx b/packages/react-components/src/components/chart/chartOptions/tooltip/time.tsx index c84705d26..28dc5385f 100644 --- a/packages/react-components/src/components/chart/chartOptions/tooltip/time.tsx +++ b/packages/react-components/src/components/chart/chartOptions/tooltip/time.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import { format } from 'date-fns'; import { fontWeightHeadingS } from '@cloudscape-design/design-tokens'; import { FULL_DATE } from '../../../../utils/time'; +import { DateTime } from '../../../timeZone'; export type XYPlotTooltipTimeOptions = { time?: number; }; export const XYPlotTooltipTime = ({ time }: XYPlotTooltipTimeOptions) => ( - {time ? format(new Date(time), FULL_DATE) : ''} + {time ? : ''} ); diff --git a/packages/react-components/src/components/chart/legend/table/columnDefinitions/factory.tsx b/packages/react-components/src/components/chart/legend/table/columnDefinitions/factory.tsx index b8b9bee5f..01df66dd3 100644 --- a/packages/react-components/src/components/chart/legend/table/columnDefinitions/factory.tsx +++ b/packages/react-components/src/components/chart/legend/table/columnDefinitions/factory.tsx @@ -89,7 +89,7 @@ const createTrendCursorColumnDefinition = ({ date, }: TrendCursor): LegendTableColumnDefinitions[number] => ({ id: trendCursorId, - header: , + header: , sortingComparator: (a, b) => { const aValue = a.trendCursorValues[trendCursorId] ?? 0; const bValue = b.trendCursorValues[trendCursorId] ?? 0; diff --git a/packages/react-components/src/components/chart/legend/table/columnDefinitions/trendCursor/trendCursorHeader.tsx b/packages/react-components/src/components/chart/legend/table/columnDefinitions/trendCursor/trendCursorHeader.tsx index 35623809e..8074a5156 100644 --- a/packages/react-components/src/components/chart/legend/table/columnDefinitions/trendCursor/trendCursorHeader.tsx +++ b/packages/react-components/src/components/chart/legend/table/columnDefinitions/trendCursor/trendCursorHeader.tsx @@ -1,7 +1,10 @@ import React from 'react'; +import { parse, format } from 'date-fns'; +import { useDateTime } from '../../../../../timeZone'; +import { FULL_DATE_TIME } from '../../../../../../utils/time'; type TrendCursorColumnHeaderOptions = { - date: Date; + date: number; color?: string; }; @@ -9,12 +12,14 @@ export const TrendCursorColumnHeader = ({ date, color, }: TrendCursorColumnHeaderOptions) => { + const dateTime = useDateTime(date, FULL_DATE_TIME.replace(',', '')); + const parsedDate = parse(dateTime, 'dd/MM/yyyy HH:mm:ss', new Date()); return (
- {date.toLocaleDateString()} + {format(parsedDate, 'dd/MM/yyyy')}
- {date.toLocaleTimeString()} + {format(parsedDate, 'hh:mm:ss')}
{ expect(table).not.toBeNull(); expect( - screen.getByText(new Date(trendCursors[0].date).toLocaleTimeString()) + screen + .getAllByText(format(new Date(trendCursors[0].date), 'dd/MM/yyyy')) + .at(0) ).not.toBeNull(); expect( - screen.getByText(new Date(trendCursors[1].date).toLocaleTimeString()) + screen.getByText(format(new Date(trendCursors[1].date), 'hh:mm:ss')) ).not.toBeNull(); expect(screen.getByText('111')).not.toBeNull(); diff --git a/packages/react-components/src/components/kpi/kpi.spec.tsx b/packages/react-components/src/components/kpi/kpi.spec.tsx index 8e26b303d..bf84a2ec7 100644 --- a/packages/react-components/src/components/kpi/kpi.spec.tsx +++ b/packages/react-components/src/components/kpi/kpi.spec.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { mockTimeSeriesDataQuery } from '@iot-app-kit/testing-util'; +import { format } from 'date-fns-tz'; import { KPI } from './kpi'; +import { FULL_DATE_TIME } from '../../utils/time'; const VIEWPORT = { duration: '5m' }; @@ -35,6 +37,6 @@ it('renders', async () => { `${DATA_STREAM.data[0].y} ` ); expect(screen.getByTestId('kpi-timestamp').textContent).toContain( - new Date(DATA_STREAM.data[0].x).toLocaleString() + format(new Date(DATA_STREAM.data[0].x), FULL_DATE_TIME) ); }); diff --git a/packages/react-components/src/components/kpi/kpiBase.spec.tsx b/packages/react-components/src/components/kpi/kpiBase.spec.tsx index c3dfd90a2..bc402e104 100644 --- a/packages/react-components/src/components/kpi/kpiBase.spec.tsx +++ b/packages/react-components/src/components/kpi/kpiBase.spec.tsx @@ -1,7 +1,9 @@ import React from 'react'; +import { format } from 'date-fns-tz'; import { render, screen } from '@testing-library/react'; import { KpiBase } from './kpiBase'; import type { DataPoint } from '@iot-app-kit/core'; +import { FULL_DATE_TIME } from '../../utils/time'; describe('name', () => { it('renders name when showName is true', () => { @@ -149,7 +151,7 @@ describe('timestamp', () => { ); expect( - screen.queryByText(PROPERTY_POINT_DATE.toLocaleString()) + screen.queryByText(format(PROPERTY_POINT_DATE, FULL_DATE_TIME)) ).not.toBeNull(); }); diff --git a/packages/react-components/src/components/kpi/kpiBase.tsx b/packages/react-components/src/components/kpi/kpiBase.tsx index a15474f06..5856e2fc2 100644 --- a/packages/react-components/src/components/kpi/kpiBase.tsx +++ b/packages/react-components/src/components/kpi/kpiBase.tsx @@ -15,6 +15,7 @@ import { fontSizeBodyS, } from '@cloudscape-design/design-tokens'; import { DataQualityText } from '../data-quality/data-quality-text'; +import { DateTime } from '../timeZone'; export const KpiBase: React.FC = ({ propertyPoint, @@ -138,7 +139,7 @@ export const KpiBase: React.FC = ({ }} />
- {isLoading ? '-' : new Date(point.x).toLocaleString()} + {isLoading ? '-' : }
)} diff --git a/packages/react-components/src/components/timeZone/index.ts b/packages/react-components/src/components/timeZone/index.ts new file mode 100644 index 000000000..874fe61c7 --- /dev/null +++ b/packages/react-components/src/components/timeZone/index.ts @@ -0,0 +1 @@ +export * from './timeZone'; diff --git a/packages/react-components/src/components/timeZone/timeZone.tsx b/packages/react-components/src/components/timeZone/timeZone.tsx new file mode 100644 index 000000000..c225ea8eb --- /dev/null +++ b/packages/react-components/src/components/timeZone/timeZone.tsx @@ -0,0 +1,44 @@ +import React, { createContext, useContext } from 'react'; + +import { utcToZonedTime, format } from 'date-fns-tz'; +import { FULL_DATE_TIME } from '../../utils/time'; + +// https://date-fns.org/v3.6.0/docs/Time-Zones#date-fns-tz +// converts a utc date to a formatted string in a specific timeZone +export const formatDate = ( + dateTime: number, + { timeZone, pattern }: { timeZone: string; pattern: string } +) => { + const zonedDate = utcToZonedTime(new Date(dateTime).toISOString(), timeZone); + const formattedDate = format(zonedDate, pattern, { timeZone: timeZone }); + + return formattedDate; +}; + +// Helper components for use in a React Context +export type DateTimeFormatContextOptions = { + timeZone: string; +}; +export const DateTimeFormatContext = + createContext({ + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }); + +export type DateTimeOptions = { + dateTime: number; + pattern?: string; +}; +export const DateTime = ({ dateTime, pattern }: DateTimeOptions) => { + const formattedDate = useDateTime(dateTime, pattern); + + return <>{formattedDate}; +}; + +export const useDateTime = (dateTime: number, pattern?: string) => { + const dateTimeFormatPattern = pattern ?? FULL_DATE_TIME; + const { timeZone } = useContext(DateTimeFormatContext); + return formatDate(dateTime, { + timeZone, + pattern: dateTimeFormatPattern, + }); +}; diff --git a/packages/react-components/src/components/timestampBar/index.tsx b/packages/react-components/src/components/timestampBar/index.tsx index aa373e7b1..78a1a47fa 100644 --- a/packages/react-components/src/components/timestampBar/index.tsx +++ b/packages/react-components/src/components/timestampBar/index.tsx @@ -14,6 +14,7 @@ import { Spinner } from '@cloudscape-design/components'; import './timestamp.css'; import { useViewport } from '../../hooks/useViewport'; import { convertViewportToMs } from '../../utils/convertViewportToMs'; +import { DateTime } from '../timeZone'; type TimestampProps = { showLoadingIndicator: boolean; @@ -34,8 +35,6 @@ export const Timestamp = ({ }: TimestampProps) => { const { viewport } = useViewport(); const { initial, end } = convertViewportToMs(viewport); - const timestampStart = new Date(initial).toLocaleString(); - const timestampEnd = new Date(end).toLocaleString(); const timestampStyle = { ...styleProps, backgroundColor: showLoadingIndicator ? '' : colorBorderDividerSecondary, @@ -65,8 +64,12 @@ export const Timestamp = ({ color: colorTextBodyDefault, }} > - {timestampStart} - {timestampEnd} + + + + + +
); diff --git a/packages/react-components/src/echarts/extensions/trendCursors/view/utils/dateTime.ts b/packages/react-components/src/echarts/extensions/trendCursors/view/utils/dateTime.ts new file mode 100644 index 000000000..c64d6e285 --- /dev/null +++ b/packages/react-components/src/echarts/extensions/trendCursors/view/utils/dateTime.ts @@ -0,0 +1,10 @@ +import { formatDate } from '../../../../../components/timeZone'; + +export const getDateTime = (date: number) => { + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const formatedDate = formatDate(date, { + timeZone: timeZone, + pattern: 'dd/MM/yyyy HH:mm:ss', + }); + return formatedDate; +}; diff --git a/packages/react-components/src/echarts/extensions/trendCursors/view/utils/index.ts b/packages/react-components/src/echarts/extensions/trendCursors/view/utils/index.ts index c6ff3461a..f1bebae5e 100644 --- a/packages/react-components/src/echarts/extensions/trendCursors/view/utils/index.ts +++ b/packages/react-components/src/echarts/extensions/trendCursors/view/utils/index.ts @@ -2,3 +2,4 @@ export * from './coordinateSystem'; export * from './grid'; export * from './polyline'; export * from './xAxis'; +export * from './dateTime'; diff --git a/packages/react-components/src/echarts/extensions/trendCursors/view/view.ts b/packages/react-components/src/echarts/extensions/trendCursors/view/view.ts index 78175b07b..90a777001 100644 --- a/packages/react-components/src/echarts/extensions/trendCursors/view/view.ts +++ b/packages/react-components/src/echarts/extensions/trendCursors/view/view.ts @@ -6,7 +6,12 @@ import { SeriesModel } from 'echarts'; import Axis2D from 'echarts/types/src/coord/cartesian/Axis2D'; import { round } from '@iot-app-kit/core-util'; import useDataStore from '../../../../store'; -import { getGrid, getXAxisCoord, getXAxisDataValue } from './utils'; +import { + getGrid, + getDateTime, + getXAxisCoord, + getXAxisDataValue, +} from './utils'; import { onDeleteTrendCursor, onUpdateTrendCursor, @@ -131,9 +136,8 @@ export class TrendCursorView extends echarts.ComponentView { } toDateTimeText(date: number) { - const localeDateString = new Date(date).toLocaleDateString(); - const localeTimeString = new Date(date).toLocaleTimeString(); - const dateTimeText = `{timestamp|${localeDateString} ${localeTimeString}}`; + const dateTime = getDateTime(date); + const dateTimeText = `{timestamp|${dateTime}}`; return dateTimeText; } diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index 5a129de71..bd9febb80 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -33,6 +33,7 @@ export { useHasFeatureFlag } from './hooks/useHasFeatureFlag'; export { useGetConfigValue } from './store/index'; export { Chart } from './components/chart'; +export * from './components/timeZone'; export { AnomalyChart } from './components/anomaly-chart'; export type { diff --git a/packages/react-components/src/queries/useSiteWiseAnomalyDataSource/constants.ts b/packages/react-components/src/queries/useSiteWiseAnomalyDataSource/constants.ts index 2d4e8fa9c..19be163ec 100644 --- a/packages/react-components/src/queries/useSiteWiseAnomalyDataSource/constants.ts +++ b/packages/react-components/src/queries/useSiteWiseAnomalyDataSource/constants.ts @@ -1,5 +1,5 @@ import { HistoricalViewport } from '@iot-app-kit/core'; -import { sub } from 'date-fns/sub'; +import { sub } from 'date-fns'; export const DEFAULT_ANOMALY_DATA_SOURCE_VIEWPORT: HistoricalViewport = { start: sub(Date.now(), { days: 7 }), diff --git a/packages/react-components/src/utils/time.ts b/packages/react-components/src/utils/time.ts index 8ae5b9786..d7050caa2 100644 --- a/packages/react-components/src/utils/time.ts +++ b/packages/react-components/src/utils/time.ts @@ -13,6 +13,7 @@ export const YEAR_IN_MS = 12 * MONTH_IN_MS; // Global time format strings export const SHORT_TIME = 'hh:mm a'; export const FULL_DATE = 'yyy-MM-dd hh:mm:ss a'; +export const FULL_DATE_TIME = 'dd/MM/yyyy, hh:mm:ss'; /** * ConvertMS is a helper function that will take in milliseconds and convert it to the highest detonator