77
88import {
99 EuiButton ,
10+ EuiCallOut ,
1011 EuiFieldNumber ,
1112 EuiFilePicker ,
1213 EuiFlexGroup ,
1314 EuiFlexItem ,
1415 EuiForm ,
15- EuiSpacer ,
1616 EuiToolTip ,
1717} from '@elastic/eui' ;
1818import type { Meta , StoryFn } from '@storybook/react' ;
19- import React , { useEffect , useState } from 'react' ;
19+ import React , { useCallback , useEffect , useState } from 'react' ;
2020import { CodeEditor } from '@kbn/code-editor' ;
21+ import { useArgs } from '@storybook/preview-api' ;
22+ import {
23+ getPaths ,
24+ getServiceMapNodes ,
25+ type ServiceMapResponse ,
26+ } from '../../../../../common/service_map' ;
2127import { Cytoscape } from '../cytoscape' ;
2228import { Centerer } from './centerer' ;
2329import exampleResponseHipsterStore from './example_response_hipster_store.json' ;
@@ -26,36 +32,16 @@ import exampleResponseTodo from './example_response_todo.json';
2632import { generateServiceMapElements } from './generate_service_map_elements' ;
2733import { MockApmPluginStorybook } from '../../../../context/apm_plugin/mock_apm_plugin_storybook' ;
2834
29- const STORYBOOK_PATH = 'app/ServiceMap/Example data' ;
30-
31- const SESSION_STORAGE_KEY = `${ STORYBOOK_PATH } /pre-loaded map` ;
32- function getSessionJson ( ) {
33- return window . sessionStorage . getItem ( SESSION_STORAGE_KEY ) ;
34- }
35- function setSessionJson ( json : string ) {
36- window . sessionStorage . setItem ( SESSION_STORAGE_KEY , json ) ;
37- }
38-
3935function getHeight ( ) {
40- return window . innerHeight - 300 ;
36+ return window . innerHeight - 200 ;
4137}
4238
4339const stories : Meta < { } > = {
4440 title : 'app/ServiceMap/Example data' ,
4541 component : Cytoscape ,
46- decorators : [
47- ( StoryComponent , { globals } ) => {
48- return (
49- < MockApmPluginStorybook >
50- < StoryComponent />
51- </ MockApmPluginStorybook >
52- ) ;
53- } ,
54- ] ,
42+ decorators : [ ( wrappedStory ) => < MockApmPluginStorybook > { wrappedStory ( ) } </ MockApmPluginStorybook > ] ,
5543} ;
5644
57- export default stories ;
58-
5945export const GenerateMap : StoryFn < { } > = ( ) => {
6046 const [ size , setSize ] = useState < number > ( 10 ) ;
6147 const [ json , setJson ] = useState < string > ( '' ) ;
@@ -114,87 +100,106 @@ export const GenerateMap: StoryFn<{}> = () => {
114100 ) ;
115101} ;
116102
117- export const MapFromJSON : StoryFn < { } > = ( ) => {
118- const [ json , setJson ] = useState < string > (
119- getSessionJson ( ) || JSON . stringify ( exampleResponseTodo , null , 2 )
120- ) ;
121- const [ error , setError ] = useState < string | undefined > ( ) ;
103+ interface MapFromJSONArgs {
104+ json : unknown ;
105+ }
106+ const assertJSON : ( json ?: any ) => asserts json is ServiceMapResponse = ( json ) => {
107+ if ( ! ! json && ! ( 'elements' in json || 'spans' in json ) ) {
108+ throw new Error ( 'invalid json' ) ;
109+ }
110+ } ;
111+
112+ const MapFromJSONTemplate : StoryFn < MapFromJSONArgs > = ( args ) => {
113+ const [ { json } , updateArgs ] = useArgs ( ) ;
122114
115+ const [ error , setError ] = useState < string | undefined > ( ) ;
123116 const [ elements , setElements ] = useState < any [ ] > ( [ ] ) ;
124117
125118 const [ uniqueKeyCounter , setUniqueKeyCounter ] = useState < number > ( 0 ) ;
126- const updateRenderedElements = ( ) => {
119+ const updateRenderedElements = useCallback ( ( ) => {
127120 try {
128- setElements ( JSON . parse ( json ) . elements ) ;
121+ assertJSON ( json ) ;
122+ if ( 'elements' in json ) {
123+ setElements ( json . elements ?? [ ] ) ;
124+ } else {
125+ const paths = getPaths ( { spans : json . spans ?? [ ] } ) ;
126+ const nodes = getServiceMapNodes ( {
127+ anomalies : json . anomalies ?? {
128+ mlJobIds : [ ] ,
129+ serviceAnomalies : [ ] ,
130+ } ,
131+ connections : paths . connections ,
132+ servicesData : json . servicesData ?? [ ] ,
133+ exitSpanDestinations : paths . exitSpanDestinations ,
134+ } ) ;
135+
136+ setElements ( nodes . elements ) ;
137+ }
129138 setUniqueKeyCounter ( ( key ) => key + 1 ) ;
130139 setError ( undefined ) ;
131140 } catch ( e ) {
132141 setError ( e . message ) ;
133142 }
134- } ;
143+ } , [ json ] ) ;
135144
136145 useEffect ( ( ) => {
137146 updateRenderedElements ( ) ;
138- // eslint-disable-next-line react-hooks/exhaustive-deps
139- } , [ ] ) ;
147+ } , [ updateRenderedElements ] ) ;
140148
141149 return (
142- < div >
143- < Cytoscape key = { uniqueKeyCounter } elements = { elements } height = { getHeight ( ) } >
144- < Centerer />
145- </ Cytoscape >
146- < EuiForm isInvalid = { error !== undefined } error = { error } >
147- < EuiFlexGroup >
148- < EuiFlexItem >
149- < CodeEditor // TODO Unable to find context that provides theme. Need CODEOWNER Input
150- languageId = "json"
151- value = { json }
152- options = { { fontFamily : 'monospace' } }
153- onChange = { ( value ) => {
154- setJson ( value ) ;
155- setSessionJson ( value ) ;
156- } }
157- />
158- </ EuiFlexItem >
159- < EuiFlexItem >
160- < EuiFlexGroup direction = "column" >
161- < EuiFilePicker
162- display = { 'large' }
163- fullWidth = { true }
164- style = { { height : '100%' } }
165- initialPromptText = "Upload a JSON file"
166- onChange = { ( event ) => {
167- const item = event ?. item ( 0 ) ;
168-
169- if ( item ) {
170- const f = new FileReader ( ) ;
171- f . onload = ( onloadEvent ) => {
172- const result = onloadEvent ?. target ?. result ;
173- if ( typeof result === 'string' ) {
174- setJson ( result ) ;
175- }
176- } ;
177- f . readAsText ( item ) ;
150+ < EuiFlexGroup
151+ direction = "column"
152+ justifyContent = "spaceBetween"
153+ style = { { minHeight : '100vh' } }
154+ gutterSize = "xs"
155+ >
156+ < EuiFlexItem grow = { false } >
157+ < EuiCallOut
158+ size = "s"
159+ title = "Upload a JSON file or paste a JSON object in the Storybook Controls panel."
160+ iconType = "pin"
161+ />
162+ </ EuiFlexItem >
163+ < EuiFlexItem grow >
164+ < Cytoscape key = { uniqueKeyCounter } elements = { elements } height = { getHeight ( ) } >
165+ < Centerer />
166+ </ Cytoscape >
167+ </ EuiFlexItem >
168+ < EuiFlexItem grow = { false } >
169+ < EuiForm isInvalid = { error !== undefined } error = { error } >
170+ < EuiFilePicker
171+ display = "large"
172+ fullWidth
173+ initialPromptText = "Upload a JSON file"
174+ onChange = { ( event ) => {
175+ const item = event ?. item ( 0 ) ;
176+
177+ if ( item ) {
178+ const f = new FileReader ( ) ;
179+ f . onload = ( onloadEvent ) => {
180+ const result = onloadEvent ?. target ?. result ;
181+ if ( typeof result === 'string' ) {
182+ updateArgs ( { json : JSON . parse ( result ) } ) ;
178183 }
179- } }
180- />
181- < EuiSpacer />
182- < EuiButton
183- data-test-subj = "apmMapFromJSONRenderJsonButton"
184- onClick = { ( ) => {
185- updateRenderedElements ( ) ;
186- } }
187- >
188- Render JSON
189- </ EuiButton >
190- </ EuiFlexGroup >
191- </ EuiFlexItem >
192- </ EuiFlexGroup >
193- </ EuiForm >
194- </ div >
184+ } ;
185+ f . readAsText ( item ) ;
186+ }
187+ } }
188+ />
189+ </ EuiForm >
190+ </ EuiFlexItem >
191+ </ EuiFlexGroup >
195192 ) ;
196193} ;
197194
195+ export const MapFromJSON = MapFromJSONTemplate . bind ( { } ) ;
196+ MapFromJSON . argTypes = {
197+ json : {
198+ defaultValue : exampleResponseTodo ,
199+ control : 'object' ,
200+ } ,
201+ } ;
202+
198203export const TodoApp : StoryFn < { } > = ( ) => {
199204 return (
200205 < div >
@@ -224,3 +229,5 @@ export const HipsterStore: StoryFn<{}> = () => {
224229 </ div >
225230 ) ;
226231} ;
232+
233+ export default stories ;
0 commit comments