[ObsUX] Asset details storybook fixes (#170262)

Closes #169123 
## Summary

This PR fixes the asset details storybook. There are a lot of changes we
did that broke the existing stories - adding the feature flags, using
the URL state to select the active tab, using new services/hooks, etc.
The order of the decorators was also returning an error in the component
stories.

The background of the tab switching issue is that the active tab is set
in the URL and we can't use the query params here (they won't be set in
the storybook in case a different tab is selected)
As a workaround for the tab selection issue, I added an extra `arg` to
see all tab names and to select one of them - it's not perfect but I was
trying to avoid changing the tab switcher code only to make the story
work. One issue with that is after selecting a value in the controls the
canvas is not refreshed so I added different stories for different tabs.
It works in Docs mode:



aa6ec610-14b9-48c3-a7e4-02f57bac9390

That's the result so far: 



d7eb6ed0-f655-4576-9480-b096f5aec007


## Next steps 🐾 

The stories can be useful even if not everything inside the component is
available and the mocks do not cover every case. Supporting more cases
can become tricky so we can keep it in this minimum component view and
have the tabs available as part of the asset details story ( flyout and
page ). Tab switching can be improved as currently they can be switched
only using the Story Controls, flyout dropdown in the story, or the left
navigation (clicking on them won't work) - I explained that in the PRs
summary. We can also decide if we want to keep the stories for the asset
details as they are pretty easy to break. If we decide to support it in
the future we should keep it in mind while testing PRs.

## Update on found issues 🔍 

Other issues I see (currently not fixed) are:
- Anomalies are not rendered for the default case
- Update: I tried different things and I saw that the date change is
helping the anomalies to show (on initial load they never show - but
after a date change using the date picker they appear, even when the
same date is selected:


ce0461cc-4c2c-4134-96dc-829b480a5be1


It's really odd and I couldn't find a solution for that - As a
workaround, I removed the anomalies component story as it won't work
without the date picker.
- Logs Tab data is not visible (even without data the tab renders
without errors)
- Process list: when clicking on a process in the list there is an error
shown (related AI feature context)
- Update: This is fixed - the AI assistant component will be visible
(not functional)
<img width="1916" alt="image"
src="9d41484d-506c-4f5d-8940-57010ec11303">

## Testing

- The easy way is to click on [Storybooks
Preview](https://ci-artifacts.kibana.dev/storybooks/pr-170262/cd96eb6d6da95fa34c9b2582c353b7ecd6e80b66)
inside the PR build section
- To see the stories locally run `yarn storybook infra` and open
http://localhost:9001/
This commit is contained in:
jennypavlova 2023-11-09 13:04:25 +01:00 committed by GitHub
parent 55d94c772a
commit 4557ac83e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 144 additions and 79 deletions

View file

@ -16,7 +16,7 @@ const anomalies: GetMetricsHostsAnomaliesSuccessResponsePayload = {
actual: 758.8220213274412,
anomalyScore: 0.024881740359975164,
duration: 900,
startTime: 1486845000000,
startTime: 1681038638000,
type: 'metrics_hosts',
partitionFieldName: 'airline',
partitionFieldValue: 'NKS',
@ -42,7 +42,7 @@ const anomalies: GetMetricsHostsAnomaliesSuccessResponsePayload = {
actual: 758.8220213274412,
anomalyScore: 100.024881740359975164,
duration: 900,
startTime: 1486845000000,
startTime: 1681038638000,
type: 'metrics_hosts',
partitionFieldName: 'airline',
partitionFieldValue: 'NKS',

View file

@ -16,12 +16,6 @@ const tabs: Tab[] = [
defaultMessage: 'Overview',
}),
},
{
id: ContentTabIds.LOGS,
name: i18n.translate('xpack.infra.nodeDetails.tabs.logs', {
defaultMessage: 'Logs',
}),
},
{
id: ContentTabIds.METADATA,
name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.metadata', {
@ -34,6 +28,12 @@ const tabs: Tab[] = [
defaultMessage: 'Processes',
}),
},
{
id: ContentTabIds.LOGS,
name: i18n.translate('xpack.infra.nodeDetails.tabs.logs', {
defaultMessage: 'Logs',
}),
},
{
id: ContentTabIds.ANOMALIES,
name: i18n.translate('xpack.infra.nodeDetails.tabs.anomalies', {

View file

@ -11,24 +11,33 @@ import {
KibanaContextProvider,
type KibanaReactContextValue,
} from '@kbn/kibana-react-plugin/public';
import { of } from 'rxjs';
import { Observable, of } from 'rxjs';
import { action } from '@storybook/addon-actions';
import type { DecoratorFn } from '@storybook/react';
import { useParameter } from '@storybook/addons';
import type { DeepPartial } from 'utility-types';
import type { LocatorPublic } from '@kbn/share-plugin/public';
import type { IKibanaSearchRequest, ISearchOptions } from '@kbn/data-plugin/public';
import type {
IKibanaSearchRequest,
ISearchOptions,
SearchSessionState,
} from '@kbn/data-plugin/public';
import { AlertSummaryWidget } from '@kbn/triggers-actions-ui-plugin/public/application/sections/alert_summary_widget/alert_summary_widget';
import type { Theme } from '@elastic/charts/dist/utils/themes/theme';
import type { AlertSummaryWidgetProps } from '@kbn/triggers-actions-ui-plugin/public/application/sections/alert_summary_widget';
import { defaultLogViewAttributes } from '@kbn/logs-shared-plugin/common';
import { DataView, DataViewField } from '@kbn/data-views-plugin/common';
import { MemoryRouter } from 'react-router-dom';
import { ObservabilityAIAssistantProvider } from '@kbn/observability-ai-assistant-plugin/public';
import { ObservabilityAIAssistantService } from '@kbn/observability-ai-assistant-plugin/public/types';
import { PluginConfigProvider } from '../../../containers/plugin_config_context';
import type { PluginKibanaContextValue } from '../../../hooks/use_kibana';
import { SourceProvider } from '../../../containers/metrics_source';
import { getHttp } from './context/http';
import { assetDetailsProps, getLogEntries } from './context/fixtures';
import { ContextProviders } from '../context_providers';
import { DataViewsProvider } from '../hooks/use_data_views';
import type { InfraConfig } from '../../../../server';
const settings: Record<string, any> = {
'dateFormat:scaled': [['', 'HH:mm:ss.SSS']],
@ -58,6 +67,10 @@ export const DecorateWithKibanaContext: DecoratorFn = (story) => {
search: (request: IKibanaSearchRequest, options?: ISearchOptions) => {
return getLogEntries(request, options) as any;
},
session: {
start: () => 'started',
state$: { closed: false } as unknown as Observable<SearchSessionState>,
},
},
query: {
filterManager: {
@ -144,14 +157,64 @@ export const DecorateWithKibanaContext: DecoratorFn = (story) => {
},
telemetry: {
reportAssetDetailsFlyoutViewed: () => {},
reportAssetDetailsPageViewed: () => {},
},
};
const config: InfraConfig = {
alerting: {
inventory_threshold: {
group_by_page_size: 11,
},
metric_threshold: {
group_by_page_size: 11,
},
},
enabled: true,
inventory: {
compositeSize: 11,
},
sources: {
default: {
fields: {
message: ['default'],
},
},
},
featureFlags: {
customThresholdAlertsEnabled: true,
logsUIEnabled: false,
metricsExplorerEnabled: false,
osqueryEnabled: true,
inventoryThresholdAlertRuleEnabled: true,
metricThresholdAlertRuleEnabled: true,
logThresholdAlertRuleEnabled: true,
alertsAndRulesDropdownEnabled: true,
},
};
return (
<I18nProvider>
<KibanaContextProvider services={mockServices}>
<SourceProvider sourceId="default">{story()}</SourceProvider>
</KibanaContextProvider>
<MemoryRouter initialEntries={['/infra/metrics/hosts']}>
<PluginConfigProvider value={config}>
<KibanaContextProvider services={mockServices}>
<ObservabilityAIAssistantProvider
value={
{
isEnabled: () => true,
callApi: () => {},
getCurrentUser: () => {},
getLicense: () => {},
getLicenseManagementLocator: () => {},
start: {},
} as unknown as ObservabilityAIAssistantService
}
>
<SourceProvider sourceId="default">{story()}</SourceProvider>
</ObservabilityAIAssistantProvider>
</KibanaContextProvider>
</PluginConfigProvider>
</MemoryRouter>
</I18nProvider>
);
};

View file

@ -6,19 +6,32 @@
*/
import React, { useState } from 'react';
import { EuiButton } from '@elastic/eui';
import { EuiButton, EuiCallOut, EuiSelect, EuiSpacer } from '@elastic/eui';
import type { Meta, Story } from '@storybook/react/types-6-0';
import { MemoryRouter } from 'react-router-dom';
import { useArgs } from '@storybook/addons';
import { AssetDetails } from './asset_details';
import { decorateWithGlobalStorybookThemeProviders } from '../../test_utils/use_global_storybook_theme';
import { type AssetDetailsProps } from './types';
import { type TabIds, type AssetDetailsProps } from './types';
import { DecorateWithKibanaContext } from './__stories__/decorator';
import { assetDetailsProps } from './__stories__/context/fixtures';
const stories: Meta<AssetDetailsProps> = {
interface AssetDetailsStoryArgs extends AssetDetailsProps {
tabId: TabIds;
}
const stories: Meta<AssetDetailsStoryArgs> = {
title: 'infra/Asset Details View',
decorators: [decorateWithGlobalStorybookThemeProviders, DecorateWithKibanaContext],
component: AssetDetails,
argTypes: {
tabId: {
options: assetDetailsProps.tabs.filter(({ id }) => id !== 'linkToApm').map(({ id }) => id),
defaultValue: 'overview',
control: {
type: 'radio',
},
},
links: {
options: assetDetailsProps.links,
control: {
@ -26,20 +39,42 @@ const stories: Meta<AssetDetailsProps> = {
},
},
},
args: {
...assetDetailsProps,
},
args: { ...assetDetailsProps },
};
const PageTemplate: Story<AssetDetailsProps> = (args) => {
return <AssetDetails {...args} />;
const PageTabTemplate: Story<AssetDetailsStoryArgs> = (args) => {
return (
<MemoryRouter initialEntries={[`/infra/metrics/hosts?assetDetails=(tabId:${args.tabId})`]}>
<AssetDetails {...args} />
</MemoryRouter>
);
};
const FlyoutTemplate: Story<AssetDetailsProps> = (args) => {
const FlyoutTemplate: Story<AssetDetailsStoryArgs> = (args) => {
const [isOpen, setIsOpen] = useState(false);
const closeFlyout = () => setIsOpen(false);
const options = assetDetailsProps.tabs.filter(({ id }) => id !== 'linkToApm').map(({ id }) => id);
const [{ tabId }, updateArgs] = useArgs();
return (
<div>
<EuiCallOut
color="warning"
title={`To see different tab content please close the flyout if opened, select one of the options from the drop-down and open the flyout again:`}
/>
<EuiSpacer />
<EuiSelect
data-test-subj="infraFlyoutTemplateSelect"
value={tabId}
onChange={(e) => {
updateArgs({ tabId: e.target.value as TabIds });
}}
options={options.map((id) => ({
text: id,
value: id,
}))}
/>
<EuiSpacer />
<EuiButton
data-test-subj="infraFlyoutTemplateOpenFlyoutButton"
onClick={() => setIsOpen(true)}
@ -47,13 +82,33 @@ const FlyoutTemplate: Story<AssetDetailsProps> = (args) => {
Open flyout
</EuiButton>
<div hidden={!isOpen}>
{isOpen && <AssetDetails {...args} renderMode={{ mode: 'flyout', closeFlyout }} />}
{isOpen && (
<MemoryRouter
key={tabId}
initialEntries={[`/infra/metrics/hosts?assetDetails=(tabId:${tabId ?? args?.tabId})`]}
>
<AssetDetails {...args} renderMode={{ mode: 'flyout', closeFlyout }} />
</MemoryRouter>
)}
</div>
</div>
);
};
export const Page = PageTemplate.bind({});
export const OverviewTab = PageTabTemplate.bind({});
OverviewTab.args = { tabId: 'overview' };
export const MetadataTab = PageTabTemplate.bind({});
MetadataTab.args = { tabId: 'metadata' };
export const ProcessesTab = PageTabTemplate.bind({});
ProcessesTab.args = { tabId: 'processes' };
export const LogsTab = PageTabTemplate.bind({});
LogsTab.args = { tabId: 'logs' };
export const AnomaliesTab = PageTabTemplate.bind({});
AnomaliesTab.args = { tabId: 'anomalies' };
export const Flyout = FlyoutTemplate.bind({});

View file

@ -1,48 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type { Meta, Story } from '@storybook/react/types-6-0';
import { Anomalies } from './anomalies';
import { decorateWithGlobalStorybookThemeProviders } from '../../../../test_utils/use_global_storybook_theme';
import {
DecorateWithKibanaContext,
DecorateWithAssetDetailsStateContext,
} from '../../__stories__/decorator';
const stories: Meta = {
title: 'infra/Asset Details View/Components/Anomalies',
decorators: [
decorateWithGlobalStorybookThemeProviders,
DecorateWithKibanaContext,
DecorateWithAssetDetailsStateContext,
],
component: Anomalies,
};
const Template: Story = () => {
return <Anomalies />;
};
export const Default = Template.bind({});
export const NoData = Template.bind({});
NoData.parameters = {
apiResponse: {
mock: 'noData',
},
};
export const LoadingState = Template.bind({});
LoadingState.parameters = {
apiResponse: {
mock: 'loading',
},
};
export default stories;

View file

@ -18,8 +18,8 @@ const stories: Meta = {
title: 'infra/Asset Details View/Components/Metadata',
decorators: [
decorateWithGlobalStorybookThemeProviders,
DecorateWithKibanaContext,
DecorateWithAssetDetailsStateContext,
DecorateWithKibanaContext,
],
component: Metadata,
};
@ -30,11 +30,6 @@ const Template: Story = () => {
export const Default = Template.bind({});
export const WithActions = Template.bind({});
WithActions.args = {
showActionsColumn: true,
};
export const NoData = Template.bind({});
NoData.parameters = {
apiResponse: {

View file

@ -18,8 +18,8 @@ const stories: Meta = {
title: 'infra/Asset Details View/Components/Processes',
decorators: [
decorateWithGlobalStorybookThemeProviders,
DecorateWithKibanaContext,
DecorateWithAssetDetailsStateContext,
DecorateWithKibanaContext,
],
component: Processes,
};