From 331c08a27db5b5ad4d3b6883ebb1f2bca39c596b Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 26 Feb 2020 16:19:24 +0100 Subject: [PATCH] REMOVE loading of refs in node, load them in the browser always && ADD indicators to Sidebar --- lib/api/src/index.tsx | 2 +- lib/api/src/modules/refs.ts | 99 ++++++++++---- lib/core/src/server/common/fetch.ts | 16 --- lib/core/src/server/manager/manager-config.js | 50 ++----- lib/ui/src/components/sidebar/Refs.tsx | 124 +++++++++++++++--- 5 files changed, 188 insertions(+), 103 deletions(-) delete mode 100644 lib/core/src/server/common/fetch.ts diff --git a/lib/api/src/index.tsx b/lib/api/src/index.tsx index 4e43a9cfae4..df099549cd1 100644 --- a/lib/api/src/index.tsx +++ b/lib/api/src/index.tsx @@ -234,7 +234,7 @@ class ManagerProvider extends Component { `${url}/iframe.html`.match(source) ); - api.setRef(refId, data.stories); + api.setRef(refId, data); break; } diff --git a/lib/api/src/modules/refs.ts b/lib/api/src/modules/refs.ts index 83f58acd315..759b44cdc3d 100644 --- a/lib/api/src/modules/refs.ts +++ b/lib/api/src/modules/refs.ts @@ -1,4 +1,5 @@ -import { location } from 'global'; +import { location, fetch } from 'global'; +import { logger } from '@storybook/client-logger'; import { transformStoriesRawToStoriesHash, StoriesRaw, @@ -16,9 +17,20 @@ export interface SubState { refs: Refs; } +type Versions = string[]; + +export interface SetRefData { + stories?: StoriesRaw; + versions?: Versions; + startInjected?: true; + loginUrl?: string; + error?: any; +} + export interface SubAPI { - setRef: (id: string, stories: StoriesRaw) => void; + setRef: (id: string, data: SetRefData) => void; getRefs: () => Refs; + checkRef: (ref: InceptionRef) => Promise; } export type Mapper = (ref: InceptionRef, story: StoryInput) => StoryInput; @@ -28,7 +40,9 @@ export interface InceptionRef { url: string; startInjected?: boolean; stories: StoriesHash; + versions?: Versions; ready?: boolean; + error?: any; } export type Refs = Record; @@ -98,43 +112,78 @@ const map = (input: StoriesRaw, ref: InceptionRef, options: { mapper?: Mapper }) }; const initRefsApi = ({ store, provider }: Module) => { - const getRefs: SubAPI['getRefs'] = () => { - const { refs = {} } = provider.getConfig(); + const api: SubAPI = { + checkRef: async ref => { + const { id, url } = ref; - return refs; + const response = await fetch(`${url}/data.json`).catch(() => false); + + if (response) { + const { ok } = response; + + if (ok) { + const data: SetRefData = await response + .json() + .catch(error => ({ startInjected: true, error })); + + api.setRef(id, data); + } else { + api.setRef(id, { startInjected: true }); + } + } else { + logger.warn('an auto-injected ref threw a cors-error'); + api.setRef(id, { startInjected: true }); + } + }, + + getRefs: () => { + const { refs: fromConfig = {} } = provider.getConfig(); + const { refs: fromState = {} } = store.getState(); + + return { ...fromConfig, ...fromState }; + }, + + setRef: (id, { stories, ...rest }) => { + const ref = api.getRefs()[id]; + const after = stories + ? namespace( + transformStoriesRawToStoriesHash(map(stories, ref, { mapper: defaultMapper }), {}), + ref + ) + : undefined; + + const result = { ...ref, stories: after, ...rest, ready: true }; + + store.setState({ + refs: { + ...(store.getState().refs || {}), + [id]: result, + }, + }); + }, }; - const setRef: SubAPI['setRef'] = (id, data) => { - const ref = getRefs()[id]; - const after = namespace( - transformStoriesRawToStoriesHash(map(data, ref, { mapper: defaultMapper }), {}), - ref - ); + const refs = Object.entries(api.getRefs()); - store.setState({ - refs: { - ...(store.getState().refs || {}), - [id]: { startInjected: true, ...ref, stories: after, ready: true }, - }, - }); - }; - - const initialState: SubState['refs'] = Object.entries(getRefs()).reduce( + const initialState: SubState['refs'] = refs.reduce( (acc, [key, data]) => ({ ...acc, [key]: { - startInjected: true, + title: data.id, ...data, }, }), {} as SubState['refs'] ); + console.log(refs); + + refs.forEach(([k, v]) => { + api.checkRef(v); + }); + return { - api: { - setRef, - getRefs, - }, + api, state: { refs: initialState, }, diff --git a/lib/core/src/server/common/fetch.ts b/lib/core/src/server/common/fetch.ts deleted file mode 100644 index 3893156b5e6..00000000000 --- a/lib/core/src/server/common/fetch.ts +++ /dev/null @@ -1,16 +0,0 @@ -import fetch, { Response, RequestInfo, RequestInit } from 'node-fetch'; - -function toJson(response: Response) { - return response.json(); -} - -export default async function(url: RequestInfo, options: RequestInit = {}) { - const jsonHeaders = { 'Content-Type': 'application/json', Accept: 'application/json' }; - - Object.assign(options, { headers: Object.assign(jsonHeaders, options.headers) }); - if (options.body) { - Object.assign(options, { body: JSON.stringify(options.body) }); - } - - return fetch(url, options).then(toJson) as Promise; -} diff --git a/lib/core/src/server/manager/manager-config.js b/lib/core/src/server/manager/manager-config.js index 097217c52ab..84baeda1b4e 100644 --- a/lib/core/src/server/manager/manager-config.js +++ b/lib/core/src/server/manager/manager-config.js @@ -1,8 +1,5 @@ import path from 'path'; -import { logger } from '@storybook/node-logger'; -import { transform } from '@storybook/api/dist/modules/refs'; import loadPresets from '../presets'; -import fetch from '../common/fetch'; import loadCustomPresets from '../common/custom-presets'; async function getManagerWebpackConfig(options, presets) { @@ -11,46 +8,15 @@ async function getManagerWebpackConfig(options, presets) { const entries = await presets.apply('managerEntries', [], options); if (refs) { - const out = await Promise.all( - Object.entries(refs).map(async ([key, value]) => { - const url = typeof value === 'string' ? value : value.url; - const dataUrl = `${url}/data.json`; + Object.entries(refs).forEach(([key, value]) => { + const url = typeof value === 'string' ? value : value.url; + const title = typeof value === 'string' ? key : value.title; - logger.warn(`searching for data of ref: ${key} at: ${dataUrl}`); - - const data = await fetch(dataUrl) - .then(r => { - if (r.error) { - throw new Error(''); - } - - return r; - }) - .catch(e => { - return false; - }); - - refs[key] = { - id: key, - url, - ...(data || {}), - }; - - return [key, !!data, data]; - }) - ); - - out.forEach(([key, hasData, data]) => { - if (!hasData) { - logger.warn( - `ref with id: "${key}" did not have data, the frame will be loaded immediately, which is bad for storybook performance` - ); - - refs[key].startInjected = true; - } else { - refs[key].startInjected = false; - refs[key].stories = transform(refs[key].stories, refs[key]); - } + refs[key] = { + id: key, + title, + url, + }; }); entries.push(path.resolve(path.join(options.configDir, `generated-refs.js`))); diff --git a/lib/ui/src/components/sidebar/Refs.tsx b/lib/ui/src/components/sidebar/Refs.tsx index 5e8fa5d3643..211a382260b 100644 --- a/lib/ui/src/components/sidebar/Refs.tsx +++ b/lib/ui/src/components/sidebar/Refs.tsx @@ -13,6 +13,7 @@ import { transparentize } from 'polished'; import { styled } from '@storybook/theming'; import { StoriesHash, State, useStorybookApi, isRoot } from '@storybook/api'; +import { Icons, WithTooltip, TooltipMessage } from '@storybook/components'; import { ListItem } from './Tree/ListItem'; import { toFiltered, getMains, getParents } from './Tree/utils'; import { Tree } from './Tree/Tree'; @@ -25,6 +26,8 @@ type BooleanSet = Record; type Item = StoriesHash[keyof StoriesHash]; type DataSet = Record; +type FilteredType = 'filtered' | 'unfiltered'; + interface RefProps { storyId: string; filter: string; @@ -93,14 +96,87 @@ const Components = { List: styled.div({}), }; -export const Ref: FunctionComponent = ({ - stories, - id: key, - title = key, - storyId, - filter, - isHidden = false, -}) => { +const ProblemPlacement = styled.div({ + position: 'absolute', + top: 0, + right: 10, + width: 20, + height: 20, +}); + +const getTitle = (ref: RefType) => { + switch (true) { + case !!ref.error: { + return 'an error occured'; + } + case !!ref.startInjected: { + return 'auto injected'; + } + default: { + return ref.title; + } + } +}; +const getDescription = (ref: RefType) => { + switch (true) { + case !!ref.error: { + return
{ref.error.toString()}
; + } + case !!ref.startInjected: { + return ( +
+

this storybook was auto-injected,

+

this is bad for performance, please do XYZ

+
+ ); + } + default: { + return ( +
+ this ref was loaded from {ref.url} +
+ ); + } + } +}; +const getIcon = (ref: RefType) => { + switch (true) { + case !!ref.error: { + return ; + } + case !!ref.startInjected: { + return ; + } + default: { + return ; + } + } +}; + +const RefIndicator: FunctionComponent = ref => { + const title = getTitle(ref); + const description = getDescription(ref); + const icon = getIcon(ref); + + return ( + + } + > + {icon} + + + ); +}; + +const Wrapper = styled.div({ + position: 'relative', +}); + +export const Ref: FunctionComponent = ref => { + const { stories, id: key, title = key, storyId, filter, isHidden = false } = ref; const { dataSet, expandedSet, length, others, roots, setExpanded, selectedSet } = useDataset( stories, filter, @@ -118,7 +194,8 @@ export const Ref: FunctionComponent = ({ const isMain = key === 'storybook_internal'; return ( -
+ + {!isMain ? : null} {!isMain ? {title} : null} {isLoading ? ( @@ -160,12 +237,10 @@ export const Ref: FunctionComponent = ({ )} -
+ ); }; -type FilteredType = 'filtered' | 'unfiltered'; - const useExpanded = ( type: FilteredType, parents: Item[], @@ -231,14 +306,25 @@ const useFiltered = (dataset: DataSet, filter: string, parents: Item[], storyId: }; const useDataset = (dataset: DataSet = {}, filter: string, storyId: string) => { + const emptyInitial = useMemo( + () => ({ + filtered: {}, + unfiltered: {}, + }), + [] + ); + const datasetKeys = Object.keys(dataset); const initial = useMemo(() => { - return Object.keys(dataset).reduce( - (acc, k) => ({ - filtered: { ...acc.filtered, [k]: true }, - unfiltered: { ...acc.unfiltered, [k]: false }, - }), - { filtered: {} as BooleanSet, unfiltered: {} as BooleanSet } - ); + if (datasetKeys.length) { + return Object.keys(dataset).reduce( + (acc, k) => ({ + filtered: { ...acc.filtered, [k]: true }, + unfiltered: { ...acc.unfiltered, [k]: false }, + }), + { filtered: {} as BooleanSet, unfiltered: {} as BooleanSet } + ); + } + return emptyInitial; }, [dataset]); const type: FilteredType = filter.length >= 2 ? 'filtered' : 'unfiltered';