diff --git a/core/instrument/src/babel/extract-component.ts b/core/instrument/src/babel/extract-component.ts index 49148c090..6b15a50ae 100644 --- a/core/instrument/src/babel/extract-component.ts +++ b/core/instrument/src/babel/extract-component.ts @@ -152,7 +152,10 @@ export const extractStoreComponent = async ( if (store.doc) { const doc: Document = store.doc; if (doc.componentsLookup) { - const componentNames = Object.keys(doc.componentsLookup); + const componentNames = Object.keys({ + ...doc.componentsLookup, + [doc.component as string]: doc.component, + }); if (componentNames) { for (const componentName of componentNames) { const { component, componentPackage } = await extractComponent( diff --git a/examples/gatsby/.config/buildtime.js b/examples/gatsby/.config/buildtime.js index 453184547..9d5126076 100644 --- a/examples/gatsby/.config/buildtime.js +++ b/examples/gatsby/.config/buildtime.js @@ -18,7 +18,9 @@ module.exports = { '../../stories/src/stories_native/*.stories.@(js|jsx|tsx|mdx)', '../../stories/src/mdx-stories/*.mdx', '../../../ui/app/src/**/*.stories.@(js|jsx|tsx|mdx)', + '../../../ui/components/src/**/*.mdx', '../../../ui/components/src/**/*.stories.@(js|jsx|tsx|mdx)', + '../../../ui/blocks/src/**/*.mdx', '../../../ui/blocks/src/**/*.@(stories.@(js|jsx|tsx)|mdx)', '../../../ui/design-tokens/src/**/*.stories.@(js|jsx|tsx|mdx)', '../../../core/core/src/stories/**/*.stories.@(js|jsx|tsx|mdx)', diff --git a/examples/nextjs/.config/buildtime.js b/examples/nextjs/.config/buildtime.js index 1e8337f33..f0f7811d4 100644 --- a/examples/nextjs/.config/buildtime.js +++ b/examples/nextjs/.config/buildtime.js @@ -18,7 +18,9 @@ module.exports = { '../../stories/src/stories_native/*.stories.@(js|jsx|tsx|mdx)', '../../stories/src/mdx-stories/*.mdx', '../../../ui/app/src/**/*.stories.@(js|jsx|tsx|mdx)', + '../../../ui/components/src/**/*.mdx', '../../../ui/components/src/**/*.stories.@(js|jsx|tsx|mdx)', + '../../../ui/blocks/src/**/*.mdx', '../../../ui/blocks/src/**/*.@(stories.@(js|jsx|tsx)|mdx)', '../../../ui/design-tokens/src/**/*.stories.@(js|jsx|tsx|mdx)', '../../../core/core/src/stories/**/*.stories.@(js|jsx|tsx|mdx)', diff --git a/examples/stories/src/blogs/component-stats.mdx b/examples/stories/src/blogs/component-stats.mdx new file mode 100644 index 000000000..c2b6e7783 --- /dev/null +++ b/examples/stories/src/blogs/component-stats.mdx @@ -0,0 +1,63 @@ +--- +title: Introducing JSX stats +type: blog +date: 2020-12-08 +author: atanasster +route: /blogs/components-stats +description: Introducing the addon-stats - cross-reference components and attributes usage statistics for jsx +tags: + - jsx + - stats + - components usage +image: /static/components-usage-blog.jpg +component: BlockContainer +--- +import { BlockContainer } from '../../../../ui/components/src/BlockContainer/BlockContainer.tsx'; +import { ComponentJSX, Playground } from '@component-controls/blocks'; +import { ComponentUsage, AttributeUsage, ComponentUsageList, AttributesUsageList } from '@component-controls/addon-stats'; + + +# Addon stats + +We just published the new jsx instrumenting feature and related cross/usage jsx components statistics for component-controls. + +This unique feature allows you to view the jsx trees of your componemts and answer to questions such as - which of my components is the most used as a building block, and which attributes of each component are most used. + + +# JSX Tree display + +This is displayed on the documentation page of each component, where you can see a tree of jsx nodes and the attributes used in each node. + +Here is a live example of [BlockContainer](/api/components-blockcontainer--overview) component: + + + + + +# Addon-stats + +The addon stats contains several api functions, react hooks and ui elements to make it easy to display cross-usage statistics on the components in your documentation site. + +## Components usage summary + +Components usage - how many times a component is being used from another component and which of it's properties are used + + + +## Attributes usage summary + +Attributes usage - how many times an attribute is being set on a component, and the component it is being set on + + + +## Components usage details + +How many times a component is being used from another component, with a list of the components using it + + + +## Attributes usage details + +How many times an attribute is being used on a component, with a list of those components + + diff --git a/examples/stories/src/blogs/media/components-usage-blog.jpg b/examples/stories/src/blogs/media/components-usage-blog.jpg new file mode 100644 index 000000000..ff484b662 Binary files /dev/null and b/examples/stories/src/blogs/media/components-usage-blog.jpg differ diff --git a/examples/stories/src/components/Button.tsx b/examples/stories/src/components/Button.tsx index da616f21e..f12b6b187 100644 --- a/examples/stories/src/components/Button.tsx +++ b/examples/stories/src/components/Button.tsx @@ -67,7 +67,7 @@ export const Button: FC = ({ padding, }} > - {children} +
{children}
); diff --git a/plugins/addon-stats/.config/buildtime.js b/plugins/addon-stats/.config/buildtime.js new file mode 100644 index 000000000..9e2516647 --- /dev/null +++ b/plugins/addon-stats/.config/buildtime.js @@ -0,0 +1,5 @@ +module.exports = { + stories: [ + '../src/**/*.stories.tsx', + ], +}; \ No newline at end of file diff --git a/plugins/addon-stats/.eslintignore b/plugins/addon-stats/.eslintignore new file mode 100644 index 000000000..53c37a166 --- /dev/null +++ b/plugins/addon-stats/.eslintignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/plugins/addon-stats/LICENSE.md b/plugins/addon-stats/LICENSE.md new file mode 100644 index 000000000..a64cf9401 --- /dev/null +++ b/plugins/addon-stats/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Atanas Stoyanov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/plugins/addon-stats/README.md b/plugins/addon-stats/README.md new file mode 100644 index 000000000..4498f3232 --- /dev/null +++ b/plugins/addon-stats/README.md @@ -0,0 +1,195 @@ +# Table of contents + +- [In action](#in-action) +- [Overview](#overview) +- [Getting Started](#getting-started) + - [Install](#install) + - [Usage](#usage) +- [API](#api) + - [useComponentUsageAggregate](#insusecomponentusageaggregateins) + - [useAttributesUsageAggregate](#insuseattributesusageaggregateins) + - [AttributeUsage](#insattributeusageins) + - [AttributesUsageDetails](#insattributesusagedetailsins) + - [AttributesUsageList](#insattributesusagelistins) + - [ComponentUsage](#inscomponentusageins) + - [ComponentUsageDetails](#inscomponentusagedetailsins) + - [ComponentUsageList](#inscomponentusagelistins) + +# In action + +[Example site](https://component-controls.com/api/components-actioncontainer--overview/viewport) + +# Overview + +Addon to collect and display statistics for component-controls + +# Getting Started + +## Install + +```sh +yarn add @component-controls/addon-stats --dev +``` + +## Usage + + +``` +import { ComponentUsage, AttributeUsage, ComponentUsageList, AttributesUsageList } from '@component-controls/addon-stats'; + + +## Attributes usage summary + +Attributes usage - how many times an attribute is being set on a component, and on which component it is being set + + + +## Components usage details + +How many times a component is being used from another component, with a list of the components using it + + + +## Attributes usage details + +How many times an attribute is being used on a component, with a list of those components + + + +``` + +# API + + + + + +## useComponentUsageAggregate + +_useComponentUsageAggregate [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/hooks/components.ts)_ + +### properties + +| Name | Type | Description | +| --------- | ------------- | ----------- | +| `filter*` | _StatsFilter_ | | + +## useAttributesUsageAggregate + +_useAttributesUsageAggregate [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/hooks/components.ts)_ + +### properties + +| Name | Type | Description | +| --------- | ------------- | ----------- | +| `filter*` | _StatsFilter_ | | + +## AttributeUsage + +_AttributeUsage [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/ui/AttributeUsage/AttributeUsage.tsx)_ + +### properties + +| Name | Type | Description | +| ------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `filter*` | _StatsFilter_ | | +| `title` | _string_ | optional section title for the block. | +| `description` | _string_ | optional markdown description. | +| `id` | _string_ | optional id to be used for the block if no id is provided, one will be calculated automatically from the title. | +| `collapsible` | _boolean_ | if false, will nothave a collapsible frame. | +| `data-testid` | _string_ | testing id | +| `plain` | _boolean_ | inner container variant. default to 'inner' to display a border and shadow | +| `sx` | _ThemeUIStyleObject_ | | +| `ref` | _((instance: HTMLDivElement) => void) \| RefObject<HTMLDivElement>_ | | + +## AttributesUsageDetails + +_AttributesUsageDetails [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/ui/AttributesUsageDetails/AttributesUsageDetails.tsx)_ + +### properties + +| Name | Type | Description | +| ------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `stats*` | _AttributeAggregateRow_ | | +| `title` | _string_ | optional section title for the block. | +| `description` | _string_ | optional markdown description. | +| `id` | _string_ | optional id to be used for the block if no id is provided, one will be calculated automatically from the title. | +| `collapsible` | _boolean_ | if false, will nothave a collapsible frame. | +| `data-testid` | _string_ | testing id | +| `plain` | _boolean_ | inner container variant. default to 'inner' to display a border and shadow | +| `sx` | _ThemeUIStyleObject_ | | +| `ref` | _((instance: HTMLDivElement) => void) \| RefObject<HTMLDivElement>_ | | + +## AttributesUsageList + +_AttributesUsageList [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/ui/AttributesUsageList/AttributesUsageList.tsx)_ + +### properties + +| Name | Type | Description | +| ------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `filter*` | _StatsFilter_ | | +| `title` | _string_ | optional section title for the block. | +| `description` | _string_ | optional markdown description. | +| `id` | _string_ | optional id to be used for the block if no id is provided, one will be calculated automatically from the title. | +| `collapsible` | _boolean_ | if false, will nothave a collapsible frame. | +| `data-testid` | _string_ | testing id | +| `plain` | _boolean_ | inner container variant. default to 'inner' to display a border and shadow | +| `sx` | _ThemeUIStyleObject_ | | +| `ref` | _((instance: HTMLDivElement) => void) \| RefObject<HTMLDivElement>_ | | + +## ComponentUsage + +_ComponentUsage [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/ui/ComponentUsage/ComponentUsage.tsx)_ + +### properties + +| Name | Type | Description | +| ------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `filter*` | _StatsFilter_ | | +| `title` | _string_ | optional section title for the block. | +| `description` | _string_ | optional markdown description. | +| `id` | _string_ | optional id to be used for the block if no id is provided, one will be calculated automatically from the title. | +| `collapsible` | _boolean_ | if false, will nothave a collapsible frame. | +| `data-testid` | _string_ | testing id | +| `plain` | _boolean_ | inner container variant. default to 'inner' to display a border and shadow | +| `sx` | _ThemeUIStyleObject_ | | +| `ref` | _((instance: HTMLDivElement) => void) \| RefObject<HTMLDivElement>_ | | + +## ComponentUsageDetails + +_ComponentUsageDetails [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/ui/ComponentUsageDetails/ComponentUsageDetails.tsx)_ + +### properties + +| Name | Type | Description | +| ------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `stats*` | _ComponentStats_ | | +| `title` | _string_ | optional section title for the block. | +| `description` | _string_ | optional markdown description. | +| `id` | _string_ | optional id to be used for the block if no id is provided, one will be calculated automatically from the title. | +| `collapsible` | _boolean_ | if false, will nothave a collapsible frame. | +| `data-testid` | _string_ | testing id | +| `plain` | _boolean_ | inner container variant. default to 'inner' to display a border and shadow | +| `sx` | _ThemeUIStyleObject_ | | +| `ref` | _((instance: HTMLDivElement) => void) \| RefObject<HTMLDivElement>_ | | + +## ComponentUsageList + +_ComponentUsageList [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/ui/ComponentUsageList/ComponentUsageList.tsx)_ + +### properties + +| Name | Type | Description | +| ------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `filter*` | _StatsFilter_ | | +| `title` | _string_ | optional section title for the block. | +| `description` | _string_ | optional markdown description. | +| `id` | _string_ | optional id to be used for the block if no id is provided, one will be calculated automatically from the title. | +| `collapsible` | _boolean_ | if false, will nothave a collapsible frame. | +| `data-testid` | _string_ | testing id | +| `plain` | _boolean_ | inner container variant. default to 'inner' to display a border and shadow | +| `sx` | _ThemeUIStyleObject_ | | +| `ref` | _((instance: HTMLDivElement) => void) \| RefObject<HTMLDivElement>_ | | + + diff --git a/plugins/addon-stats/package.json b/plugins/addon-stats/package.json new file mode 100644 index 000000000..914f867e1 --- /dev/null +++ b/plugins/addon-stats/package.json @@ -0,0 +1,59 @@ +{ + "name": "@component-controls/addon-stats", + "version": "2.1.0", + "description": "Component controls stats addon", + "keywords": [ + "addon", + "stats" + ], + "main": "dist/index.js", + "module": "dist/index.esm.js", + "typings": "dist/index.d.ts", + "files": [ + "dist/", + "package.json", + "README.md" + ], + "scripts": { + "build": "yarn cross-env NODE_ENV=production rollup -c", + "dev": "yarn cross-env NODE_ENV=development rollup -cw", + "docs": "ts-md", + "fix": "yarn lint --fix", + "lint": "yarn eslint . --ext mdx,ts,tsx", + "prepare": "yarn build", + "test": "cc-jest -c ./.config" + }, + "homepage": "https://github.com/ccontrols/component-controls", + "bugs": { + "url": "https://github.com/ccontrols/component-controls/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/ccontrols/component-controls.git", + "directory": "plugins/viewport-plugin" + }, + "license": "MIT", + "dependencies": { + "@component-controls/blocks": "^2.1.0", + "@component-controls/components": "^2.1.0", + "@component-controls/core": "^2.1.0" + }, + "devDependencies": { + "@types/react": "^16.9.34", + "react": "^17.0.1", + "react-table": "^7.0.0", + "theme-ui": "^0.6.0-alpha.1", + "typescript": "^4.0.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17", + "react-table": ">= 7.0.0", + "theme-ui": ">= 0.4.0-rc.1" + }, + "publishConfig": { + "access": "public" + }, + "authors": [ + "Atanas Stoyanov" + ] +} diff --git a/plugins/addon-stats/rollup.config.js b/plugins/addon-stats/rollup.config.js new file mode 100644 index 000000000..ba1c61c86 --- /dev/null +++ b/plugins/addon-stats/rollup.config.js @@ -0,0 +1,5 @@ +import { config } from '../../rollup-config'; + +export default config({ + input: ['./src/index.ts'], +}); diff --git a/plugins/addon-stats/src/api/components.ts b/plugins/addon-stats/src/api/components.ts new file mode 100644 index 000000000..b671413ac --- /dev/null +++ b/plugins/addon-stats/src/api/components.ts @@ -0,0 +1,232 @@ +import { + Store, + getComponentName, + JSXTree, + defaultExport, +} from '@component-controls/core'; +import { + ComponentStatsList, + StatsFilter, + ComponentAggregateStats, + AttributeAggregateRow, + AttributeAggregateStats, +} from '../types'; + +export const getComponentUsageStats = ( + store: Store, + filter?: StatsFilter, +): ComponentStatsList => { + const stats: ComponentStatsList = Object.keys(store.stories).reduce( + (acc: ComponentStatsList, storyId: string): ComponentStatsList => { + const story = store.stories[storyId]; + const { doc: docId = '' } = story; + const doc = store.docs[docId]; + if (doc && story.component) { + const componentName = getComponentName(story.component) || ''; + const componentHash = doc.componentsLookup?.[componentName] || ''; + const component = store.components[componentHash]; + if ( + component && + (typeof filter !== 'function' || filter({ component, doc })) + ) { + if (acc[componentHash] !== undefined) { + return { + ...acc, + [componentHash]: { + ...acc[componentHash], + stories: [...acc[componentHash].stories, story], + }, + }; + } else { + return { + ...acc, + [componentHash]: { + name: store.components[componentHash].name, + stories: [story], + usedBy: {}, + attributes: {}, + }, + }; + } + } + } + return acc; + }, + {}, + ); + const jsxTreeStats = ( + componentHash: string, + tree: JSXTree, + stats: ComponentStatsList, + ) => { + if (tree.length) { + tree.forEach(item => { + let stat = stats[item.componentKey || '']; + if (!stat && item.importedName === defaultExport) { + const docHash = Object.keys(stats).find( + key => stats[key].name === item.name, + ); + if (docHash) { + stat = stats[docHash]; + } + } + if (stat) { + if (stat.usedBy[componentHash] !== undefined) { + stat.usedBy[componentHash].count = + stat.usedBy[componentHash].count + 1; + } else { + stat.usedBy[componentHash] = { + count: 1, + node: item, + }; + } + if (item.attributes) { + item.attributes.forEach(attr => { + if (stat.attributes[attr] !== undefined) { + stat.attributes[attr] = stat.attributes[attr] + 1; + } else { + stat.attributes[attr] = 1; + } + }); + } + } + if (item.children) { + jsxTreeStats(componentHash, item.children, stats); + } + }); + } + }; + Object.keys(stats).forEach(hash => { + const component = store.components[hash]; + if (component) { + const { jsx } = component; + if (jsx) { + jsxTreeStats(hash, jsx, stats); + } + } + }); + return stats; +}; + +export const getComponentUsageAggregate = ( + store: Store, + stats: ComponentStatsList, +): ComponentAggregateStats => { + return { + data: Object.keys(stats).map(key => { + const component = stats[key]; + const storiesCount = component.stories.length; + const usedByCount = Object.keys(component.usedBy).reduce( + (acc, item) => acc + component.usedBy[item].count, + 0, + ); + const attributesCount = Object.keys(component.attributes).reduce( + (acc, item) => acc + component.attributes[item], + 0, + ); + return { + component: store.components[key], + attributesCount, + storiesCount, + usedByCount, + stats: stats[key], + componentHash: key, + }; + }), + maxStories: Object.keys(stats).reduce( + (acc, key) => Math.max(acc, stats[key].stories.length), + 0, + ), + maxUsed: Object.keys(stats).reduce( + (acc, key) => + Math.max( + acc, + Object.keys(stats[key].usedBy).reduce( + (acc, item) => acc + stats[key].usedBy[item].count, + 0, + ), + ), + 0, + ), + maxAttributes: Object.keys(stats).reduce( + (acc, key) => + Math.max( + acc, + Object.keys(stats[key].attributes).reduce( + (acc, item) => acc + stats[key].attributes[item], + 0, + ), + ), + 0, + ), + }; +}; + +export const getAttributeUsageAggregate = ( + store: Store, + stats: ComponentStatsList, +): AttributeAggregateStats => { + const data: Record = Object.keys(stats).reduce( + (acc, key) => { + const component = stats[key]; + const attributes = Object.keys(component.attributes).reduce( + (acc: Record, attribute) => { + const newItem: AttributeAggregateRow = acc[attribute] + ? { + attribute, + components: { + ...acc[attribute].components, + [key]: { + name: store.components[key].name, + count: + (acc[attribute].components[key]?.count || 0) + + component.attributes[attribute], + }, + }, + componentsCount: + acc[attribute].componentsCount + + (acc[attribute].components[key] ? 0 : 1), + usedByCount: + acc[attribute].usedByCount + component.attributes[attribute], + } + : { + attribute, + components: { + [key]: { + count: component.attributes[attribute], + name: store.components[key].name, + }, + }, + componentsCount: 1, + usedByCount: component.attributes[attribute], + }; + return { ...acc, [attribute]: newItem }; + }, + acc, + ); + return { ...acc, ...attributes }; + }, + {}, + ); + return { + data: Object.keys(data).reduce( + (acc: AttributeAggregateRow[], item) => [...acc, data[item]], + [], + ), + maxUsed: Object.keys(data).reduce( + (acc, key) => Math.max(acc, data[key].usedByCount), + 0, + ), + maxComponentsCount: Object.keys(data).reduce( + (acc, key) => + Math.max( + acc, + Object.keys(data[key].components).reduce( + (acc, item) => acc + data[key].components[item].count, + 0, + ), + ), + 0, + ), + }; +}; diff --git a/plugins/addon-stats/src/api/index.ts b/plugins/addon-stats/src/api/index.ts new file mode 100644 index 000000000..099b463e3 --- /dev/null +++ b/plugins/addon-stats/src/api/index.ts @@ -0,0 +1 @@ +export * from './components'; \ No newline at end of file diff --git a/plugins/addon-stats/src/hooks/components.ts b/plugins/addon-stats/src/hooks/components.ts new file mode 100644 index 000000000..31a5a17a8 --- /dev/null +++ b/plugins/addon-stats/src/hooks/components.ts @@ -0,0 +1,38 @@ +import { useMemo } from 'react'; +import { useStore } from '@component-controls/store'; +import { + getComponentUsageAggregate, + getComponentUsageStats, + getAttributeUsageAggregate, +} from '../api/components'; +import { + StatsFilter, + ComponentAggregateStats, + AttributeAggregateStats, +} from '../types'; + +export const useComponentUsageAggregate = ({ + filter, +}: { + filter?: StatsFilter; +}): ComponentAggregateStats => { + const store = useStore(); + const stats = useMemo(() => { + const stats = getComponentUsageStats(store, filter); + return getComponentUsageAggregate(store, stats); + }, [filter, store]); + return stats; +}; + +export const useAttributesUsageAggregate = ({ + filter, +}: { + filter?: StatsFilter; +}): AttributeAggregateStats => { + const store = useStore(); + const stats = useMemo(() => { + const stats = getComponentUsageStats(store, filter); + return getAttributeUsageAggregate(store, stats); + }, [filter, store]); + return stats; +}; diff --git a/plugins/addon-stats/src/hooks/index.ts b/plugins/addon-stats/src/hooks/index.ts new file mode 100644 index 000000000..099b463e3 --- /dev/null +++ b/plugins/addon-stats/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './components'; \ No newline at end of file diff --git a/plugins/addon-stats/src/index.ts b/plugins/addon-stats/src/index.ts new file mode 100644 index 000000000..0ebe78b1f --- /dev/null +++ b/plugins/addon-stats/src/index.ts @@ -0,0 +1,4 @@ +export * from './types'; +export * from './api'; +export * from './hooks'; +export * from './ui'; diff --git a/plugins/addon-stats/src/types.ts b/plugins/addon-stats/src/types.ts new file mode 100644 index 000000000..f1315dba4 --- /dev/null +++ b/plugins/addon-stats/src/types.ts @@ -0,0 +1,74 @@ +import { Story, Document, Component, JSXNode } from '@component-controls/core'; + +export interface ComponentStats { + /** + * list of stories for this component + */ + stories: Story[]; + /** + * list of components that are using this component, and how many times + */ + usedBy: Record< + string, + { + count: number; + node: JSXNode; + } + >; + /** + * list of the used attributes for this component, and how many times other components are using this attribute + */ + attributes: Record; + /** + * name of the component + */ + name: string; +} + +export type ComponentStatsList = Record; + +/** + * stats filter callback function + */ +export type StatsFilter = ({ + doc, + component, +}: { + doc: Document; + component: Component; +}) => boolean; + +export interface ComponentAggregateRow { + component: Component; + componentHash: string; + stats: ComponentStats; + storiesCount: number; + usedByCount: number; + attributesCount: number; +} + +export interface ComponentAggregateStats { + data: ComponentAggregateRow[]; + maxStories: number; + maxUsed: number; + maxAttributes: number; +} + +export interface AttributeAggregateRow { + attribute: string; + components: Record< + string, + { + name: string; + count: number; + } + >; + componentsCount: number; + usedByCount: number; +} + +export interface AttributeAggregateStats { + data: AttributeAggregateRow[]; + maxUsed: number; + maxComponentsCount: number; +} diff --git a/plugins/addon-stats/src/ui/AttributeUsage/AttributeUsage.tsx b/plugins/addon-stats/src/ui/AttributeUsage/AttributeUsage.tsx new file mode 100644 index 000000000..9ed638193 --- /dev/null +++ b/plugins/addon-stats/src/ui/AttributeUsage/AttributeUsage.tsx @@ -0,0 +1,125 @@ +/* eslint-disable react/display-name */ +/** @jsx jsx */ +import { FC, useMemo } from 'react'; +import { jsx, Box } from 'theme-ui'; +import { + Table, + Column, + BlockContainer, + BlockContainerProps, + ProgressIndicator, + Tag, + Link, +} from '@component-controls/components'; +import { LocalImport } from '@component-controls/blocks'; +import { StatsFilter, AttributeAggregateRow } from '../../types'; +import { useAttributesUsageAggregate } from '../../hooks/components'; + +export type AttributeUsageProps = { + filter?: StatsFilter; + linkAttributes?: boolean; +} & BlockContainerProps; + +export const AttributeUsage: FC = ({ + filter, + linkAttributes = true, + ...rest +}) => { + const { data, maxUsed, maxComponentsCount } = useAttributesUsageAggregate({ + filter, + }); + const columns = useMemo( + () => + [ + { + Header: 'attribute', + accessor: 'attribute', + Cell: ({ + row: { + original: { attribute }, + }, + }) => ( + + {linkAttributes ? ( + {attribute} + ) : ( + attribute + )} + + ), + }, + { + Header: 'components', + width: '40%', + accessor: 'components', + Cell: ({ + row: { + original: { components }, + }, + }) => ( + + {Object.keys(components).map(key => ( + + ))} + + ), + }, + { + Header: '#used', + accessor: 'componentsCount', + isSortedDesc: true, + isSorted: true, + Cell: ({ + row: { + original: { componentsCount }, + }, + }) => ( + + ), + }, + { + Header: '#total used', + accessor: 'usedByCount', + isSortedDesc: true, + isSorted: true, + Cell: ({ + row: { + original: { usedByCount }, + }, + }) => , + }, + ] as Column[], + [maxUsed, maxComponentsCount, linkAttributes], + ); + return ( + + + sorting={true} + data={data} + sortBy={[{ id: 'usedByCount', desc: true }]} + columns={columns} + /> + + ); +}; diff --git a/plugins/addon-stats/src/ui/AttributeUsage/index.ts b/plugins/addon-stats/src/ui/AttributeUsage/index.ts new file mode 100644 index 000000000..995afd1ca --- /dev/null +++ b/plugins/addon-stats/src/ui/AttributeUsage/index.ts @@ -0,0 +1 @@ +export * from './AttributeUsage'; \ No newline at end of file diff --git a/plugins/addon-stats/src/ui/AttributesUsageDetails/AttributesUsageDetails.tsx b/plugins/addon-stats/src/ui/AttributesUsageDetails/AttributesUsageDetails.tsx new file mode 100644 index 000000000..9e59d4b1e --- /dev/null +++ b/plugins/addon-stats/src/ui/AttributesUsageDetails/AttributesUsageDetails.tsx @@ -0,0 +1,81 @@ +/* eslint-disable react/display-name */ +/** @jsx jsx */ +import { FC, useMemo } from 'react'; +import { jsx } from 'theme-ui'; +import { + Table, + Column, + BlockContainer, + BlockContainerProps, + ProgressIndicator, +} from '@component-controls/components'; +import { LocalImport } from '@component-controls/blocks'; + +import { AttributeAggregateRow } from '../../types'; + +export type AttributesUsageDetailsProps = { + stats: AttributeAggregateRow; +} & BlockContainerProps; + +export const AttributesUsageDetails: FC = ({ + stats, + ...rest +}) => { + type DataType = { + componentHash: string; + usedCount: number; + name: string; + }; + const { data, maxUsed } = useMemo(() => { + const data = Object.keys(stats.components).map(key => { + const component = stats.components[key]; + return { + componentHash: key, + usedCount: component.count, + name: component.name, + }; + }); + const maxUsed = data.reduce((acc, row) => Math.max(acc, row.usedCount), 0); + return { data, maxUsed }; + }, [stats]); + const columns = useMemo( + () => + [ + { + Header: 'component', + accessor: 'name', + Cell: ({ + row: { + original: { name, componentHash }, + }, + }) => , + }, + { + Header: '#used', + accessor: 'usedCount', + Cell: ({ + row: { + original: { usedCount }, + }, + }) => ( + + ), + }, + ] as Column[], + [maxUsed], + ); + return ( + + + sorting={true} + data={data} + sortBy={[{ id: 'usedCount', desc: true }]} + columns={columns} + /> + + ); +}; diff --git a/plugins/addon-stats/src/ui/AttributesUsageDetails/index.ts b/plugins/addon-stats/src/ui/AttributesUsageDetails/index.ts new file mode 100644 index 000000000..976948098 --- /dev/null +++ b/plugins/addon-stats/src/ui/AttributesUsageDetails/index.ts @@ -0,0 +1 @@ +export * from './AttributesUsageDetails'; \ No newline at end of file diff --git a/plugins/addon-stats/src/ui/AttributesUsageList/AttributesUsageList.tsx b/plugins/addon-stats/src/ui/AttributesUsageList/AttributesUsageList.tsx new file mode 100644 index 000000000..835018e5f --- /dev/null +++ b/plugins/addon-stats/src/ui/AttributesUsageList/AttributesUsageList.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; +import { BlockContainerProps } from '@component-controls/components'; +import { StatsFilter } from '../../types'; +import { useAttributesUsageAggregate } from '../../hooks/components'; +import { AttributesUsageDetails } from '../AttributesUsageDetails'; + +export type AttributesUsageListProps = { + filter?: StatsFilter; +} & BlockContainerProps; + +export const AttributesUsageList: FC = ({ + filter, + ...rest +}) => { + const stats = useAttributesUsageAggregate({ filter }); + return ( + <> + {stats.data + .sort((a, b) => b.usedByCount - a.usedByCount) + .map(row => ( + + ))} + + ); +}; diff --git a/plugins/addon-stats/src/ui/AttributesUsageList/index.ts b/plugins/addon-stats/src/ui/AttributesUsageList/index.ts new file mode 100644 index 000000000..02b288873 --- /dev/null +++ b/plugins/addon-stats/src/ui/AttributesUsageList/index.ts @@ -0,0 +1 @@ +export * from './AttributesUsageList'; \ No newline at end of file diff --git a/plugins/addon-stats/src/ui/ComponentUsage/ComponentUsage.tsx b/plugins/addon-stats/src/ui/ComponentUsage/ComponentUsage.tsx new file mode 100644 index 000000000..0063f288c --- /dev/null +++ b/plugins/addon-stats/src/ui/ComponentUsage/ComponentUsage.tsx @@ -0,0 +1,125 @@ +/* eslint-disable react/display-name */ +/** @jsx jsx */ +import { FC, useMemo } from 'react'; +import { jsx, Box } from 'theme-ui'; + +import { + Table, + Column, + BlockContainer, + BlockContainerProps, + ProgressIndicator, + Tag, + Link, +} from '@component-controls/components'; +import { LocalImport } from '@component-controls/blocks'; +import { ComponentAggregateRow, StatsFilter } from '../../types'; +import { useComponentUsageAggregate } from '../../hooks/components'; + +export type ComponentUsageProps = { + filter?: StatsFilter; + linkAttributes?: boolean; +} & BlockContainerProps; + +export const ComponentUsage: FC = ({ + filter, + linkAttributes = true, + ...rest +}) => { + const { data, maxStories, maxUsed } = useComponentUsageAggregate({ filter }); + const columns = useMemo( + () => + [ + { + Header: 'component', + accessor: 'component.name' as any, + Cell: ({ + row: { + original: { + component: { name }, + componentHash, + }, + }, + }: { + row: { original: ComponentAggregateRow }; + }) => , + }, + { + Header: 'attributes', + accessor: 'attributesCount', + width: '40%', + Cell: ({ + row: { + original: { + stats: { attributes }, + }, + }, + }) => ( + + {Object.keys(attributes).map(attr => ( + + {linkAttributes ? ( + {attr} + ) : ( + attr + )} + + ))} + + ), + }, + { + Header: '#stories', + id: 'storiesCount', + accessor: 'storiesCount', + Cell: ({ + row: { + original: { storiesCount }, + }, + }) => ( + + ), + }, + { + Header: '#used in', + accessor: 'usedByCount', + isSortedDesc: true, + isSorted: true, + Cell: ({ + row: { + original: { usedByCount }, + }, + }) => , + }, + ] as Column[], + [maxStories, maxUsed, linkAttributes], + ); + return ( + + + sorting={true} + data={data} + sortBy={[{ id: 'usedByCount', desc: true }]} + columns={columns} + /> + + ); +}; diff --git a/plugins/addon-stats/src/ui/ComponentUsage/index.ts b/plugins/addon-stats/src/ui/ComponentUsage/index.ts new file mode 100644 index 000000000..a4f6f4b06 --- /dev/null +++ b/plugins/addon-stats/src/ui/ComponentUsage/index.ts @@ -0,0 +1 @@ +export * from './ComponentUsage'; \ No newline at end of file diff --git a/plugins/addon-stats/src/ui/ComponentUsageDetails/ComponentUsageDetails.tsx b/plugins/addon-stats/src/ui/ComponentUsageDetails/ComponentUsageDetails.tsx new file mode 100644 index 000000000..06d9df824 --- /dev/null +++ b/plugins/addon-stats/src/ui/ComponentUsageDetails/ComponentUsageDetails.tsx @@ -0,0 +1,78 @@ +/* eslint-disable react/display-name */ +/** @jsx jsx */ +import { FC, useMemo } from 'react'; +import { jsx } from 'theme-ui'; +import { useStore } from '@component-controls/store'; +import { + Table, + Column, + BlockContainer, + BlockContainerProps, + ProgressIndicator, +} from '@component-controls/components'; +import { LocalImport } from '@component-controls/blocks'; +import { ComponentStats } from '../../types'; + +export type ComponentUsageDetailsProps = { + stats: ComponentStats; +} & BlockContainerProps; + +export const ComponentUsageDetails: FC = ({ + stats, + ...rest +}) => { + type DataType = { + componentHash: string; + usedCount: number; + name: string; + }; + const store = useStore(); + const { data, maxUsed } = useMemo(() => { + const data = Object.keys(stats.usedBy).map(key => { + const component = store.components[key]; + return { + componentHash: key, + usedCount: stats.usedBy[key].count, + name: component.name, + }; + }); + const maxUsed = data.reduce((acc, row) => Math.max(acc, row.usedCount), 0); + return { data, maxUsed }; + }, [store, stats]); + const columns = useMemo( + () => + [ + { + Header: 'component', + accessor: 'name', + Cell: ({ + row: { + original: { name, componentHash }, + }, + }) => , + }, + { + Header: '#used', + accessor: 'usedCount', + Cell: ({ + row: { + original: { usedCount }, + }, + }) => ( + + ), + }, + ] as Column[], + [maxUsed], + ); + return ( + + + + ); +}; diff --git a/plugins/addon-stats/src/ui/ComponentUsageDetails/index.ts b/plugins/addon-stats/src/ui/ComponentUsageDetails/index.ts new file mode 100644 index 000000000..3a9847390 --- /dev/null +++ b/plugins/addon-stats/src/ui/ComponentUsageDetails/index.ts @@ -0,0 +1 @@ +export * from './ComponentUsageDetails'; \ No newline at end of file diff --git a/plugins/addon-stats/src/ui/ComponentUsageList/ComponentUsageList.tsx b/plugins/addon-stats/src/ui/ComponentUsageList/ComponentUsageList.tsx new file mode 100644 index 000000000..a62c7b8f3 --- /dev/null +++ b/plugins/addon-stats/src/ui/ComponentUsageList/ComponentUsageList.tsx @@ -0,0 +1,30 @@ +import React, { FC } from 'react'; +import { BlockContainerProps } from '@component-controls/components'; +import { StatsFilter } from '../../types'; +import { useComponentUsageAggregate } from '../../hooks/components'; +import { ComponentUsageDetails } from '../ComponentUsageDetails'; + +export type ComponentUsageListProps = { + filter?: StatsFilter; +} & BlockContainerProps; + +export const ComponentUsageList: FC = ({ + filter, + ...rest +}) => { + const stats = useComponentUsageAggregate({ filter }); + return ( + <> + {stats.data + .filter(row => row.usedByCount) + .sort((a, b) => b.usedByCount - a.usedByCount) + .map(row => ( + + ))} + + ); +}; diff --git a/plugins/addon-stats/src/ui/ComponentUsageList/index.ts b/plugins/addon-stats/src/ui/ComponentUsageList/index.ts new file mode 100644 index 000000000..f909d82a1 --- /dev/null +++ b/plugins/addon-stats/src/ui/ComponentUsageList/index.ts @@ -0,0 +1 @@ +export * from './ComponentUsageList'; \ No newline at end of file diff --git a/plugins/addon-stats/src/ui/index.ts b/plugins/addon-stats/src/ui/index.ts new file mode 100644 index 000000000..555dac35f --- /dev/null +++ b/plugins/addon-stats/src/ui/index.ts @@ -0,0 +1,6 @@ +export * from './AttributeUsage'; +export * from './AttributesUsageDetails'; +export * from './AttributesUsageList'; +export * from './ComponentUsage'; +export * from './ComponentUsageDetails'; +export * from './ComponentUsageList'; \ No newline at end of file diff --git a/plugins/addon-stats/tsconfig.json b/plugins/addon-stats/tsconfig.json new file mode 100644 index 000000000..13309c708 --- /dev/null +++ b/plugins/addon-stats/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "esnext", + "declaration": true, + "resolveJsonModule": true, + "sourceMap": false, + "outDir": "./dist", + "rootDir": "./src", + "baseUrl": "./", + "typeRoots": ["../../node_modules/@types", "node_modules/@types"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules/**"] +} \ No newline at end of file diff --git a/plugins/axe-plugin/src/AllyBlock/NodesTable.tsx b/plugins/axe-plugin/src/AllyBlock/NodesTable.tsx index 29d90f0e6..4eed369e9 100644 --- a/plugins/axe-plugin/src/AllyBlock/NodesTable.tsx +++ b/plugins/axe-plugin/src/AllyBlock/NodesTable.tsx @@ -2,9 +2,13 @@ /** @jsx jsx */ import { FC, useMemo, useContext } from 'react'; import { jsx, Flex, Box, Label, Checkbox } from 'theme-ui'; -import { Column } from 'react-table'; import { NodeResult } from 'axe-core'; -import { SyntaxHighlighter, Table, Tag } from '@component-controls/components'; +import { + SyntaxHighlighter, + Table, + Column, + Tag, +} from '@component-controls/components'; import { SelectionContext, tagSelectedList, trimNode } from '../state/context'; export interface NodesTableProps { @@ -40,11 +44,11 @@ export const NodesTable: FC = ({ nodes, hideErrorColumns, }) => { - const columns: Column>[] = useMemo( + const columns: Column[] = useMemo( () => [ { Header: '', - accessor: 'selected', + accessor: 'target', width: 80, Cell: ({ row: { @@ -108,7 +112,7 @@ export const NodesTable: FC = ({ backgroundColor: 'shadow', }} > -
data={nodes} columns={columns} hiddenColumns={ diff --git a/plugins/axe-plugin/src/AllyBlock/ResultsTable.tsx b/plugins/axe-plugin/src/AllyBlock/ResultsTable.tsx index 4bcb29cf9..ae0d246af 100644 --- a/plugins/axe-plugin/src/AllyBlock/ResultsTable.tsx +++ b/plugins/axe-plugin/src/AllyBlock/ResultsTable.tsx @@ -2,7 +2,6 @@ /** @jsx jsx */ import { FC, useMemo, useCallback, useContext } from 'react'; import { jsx, Flex, Box, Text } from 'theme-ui'; -import { Column } from 'react-table'; import { ChevronRightIcon, ChevronDownIcon, @@ -15,7 +14,12 @@ import { IssueOpenedIcon, } from '@primer/octicons-react'; import { Result, ImpactValue } from 'axe-core'; -import { Table, ExternalLink, Tag } from '@component-controls/components'; +import { + Table, + Column, + ExternalLink, + Tag, +} from '@component-controls/components'; import { AxeContext } from '../state/context'; import { NodesTable } from './NodesTable'; @@ -64,7 +68,7 @@ const ResultsTable: FC = ({ results, hideErrorColumns }) => { [hideErrorColumns], ); const table = useMemo(() => { - const columns: Column>[] = [ + const columns: Column[] = [ { // Build our expander column id: 'expander', // Make sure it has an ID @@ -149,7 +153,7 @@ const ResultsTable: FC = ({ results, hideErrorColumns }) => { }, ]; return ( -
data={results || []} columns={columns} hiddenColumns={hideErrorColumns ? ['impact'] : undefined} diff --git a/plugins/viewport-plugin/README.md b/plugins/viewport-plugin/README.md index f96f13082..32fd5104b 100644 --- a/plugins/viewport-plugin/README.md +++ b/plugins/viewport-plugin/README.md @@ -32,7 +32,7 @@ yarn add @component-controls/viewport-plugin --dev ## Configure route in `.config/buildtime.js` - +``` const { defaultBuildConfig } = require('@component-controls/core'); module.exports = { @@ -48,11 +48,11 @@ in `.config/buildtime.js` }, }, } - +``` ## Configure page display in `.config/runtime.tsx` - +``` import React from 'react'; import { RunOnlyConfiguration, defaultRunConfig } from "@component-controls/core"; import { ViewportPage } from "@component-controls/viewport-plugin"; @@ -70,7 +70,7 @@ in `.config/runtime.tsx` }; export default config; - +``` # API diff --git a/plugins/viewport-plugin/package.json b/plugins/viewport-plugin/package.json index 72b887561..c4a274c10 100644 --- a/plugins/viewport-plugin/package.json +++ b/plugins/viewport-plugin/package.json @@ -30,7 +30,7 @@ "repository": { "type": "git", "url": "https://github.com/ccontrols/component-controls.git", - "directory": "plugins/viewport-plugin" + "directory": "plugins/addon-stats" }, "license": "MIT", "dependencies": { diff --git a/ui/blocks/src/ComponentDependencies/ExternalDependencies.tsx b/ui/blocks/src/ComponentDependencies/ExternalDependencies.tsx index d89233c87..b44f3aaf3 100644 --- a/ui/blocks/src/ComponentDependencies/ExternalDependencies.tsx +++ b/ui/blocks/src/ComponentDependencies/ExternalDependencies.tsx @@ -4,7 +4,7 @@ import { FC, useMemo } from 'react'; import { jsx, Flex } from 'theme-ui'; import { defaultExport, Component, ImportType } from '@component-controls/core'; import { usePackage } from '@component-controls/store'; -import { Table, Tag } from '@component-controls/components'; +import { Table, Column, Tag } from '@component-controls/components'; import { PackageLink } from '../PackageLink/PackageLink'; export interface ExternalDependenciesProps { component?: Component; @@ -17,61 +17,67 @@ export interface ExternalDependenciesProps { export const ExternalDependencies: FC = ({ component, }) => { + type DataType = { + name: string; + imports: ImportType[]; + peer: boolean; + }; const componentPackage = usePackage(component?.package); const { dependencies = {}, devDependencies = {}, peerDependencies = {} } = componentPackage || {}; const { externalDependencies: imports = {} } = component || {}; const columns = useMemo( - () => [ - { - Header: 'package', - accessor: 'name', - Cell: ({ - row: { - original: { name }, - }, - }: any) => ( - - ), - }, - { - Header: 'imports', - accessor: 'imports', - width: '70%', - Cell: ({ value }: { value: ImportType[] }) => ( - - {value.map(v => ( - - {v.importedName === defaultExport ? v.name : v.importedName} - - ))} - - ), - }, - { - Header: 'peer', - accessor: 'peer', - Cell: ({ value }: { value: boolean }) => (value ? '*' : ''), - }, - ], + () => + [ + { + Header: 'package', + accessor: 'name', + Cell: ({ + row: { + original: { name }, + }, + }: any) => ( + + ), + }, + { + Header: 'imports', + accessor: 'imports', + width: '70%', + Cell: ({ value }: { value: ImportType[] }) => ( + + {value.map(v => ( + + {v.importedName === defaultExport ? v.name : v.importedName} + + ))} + + ), + }, + { + Header: 'peer', + accessor: 'peer', + Cell: ({ value }: { value: boolean }) => (value ? '*' : ''), + }, + ] as Column[], [dependencies, devDependencies], ); - const rows = useMemo(() => { + const rows: DataType[] = useMemo(() => { const dependenciesKeys = Object.keys(dependencies); const devDependenciesKeys = Object.keys(devDependencies); const peerDependenciesKeys = @@ -113,5 +119,5 @@ export const ExternalDependencies: FC = ({ return null; } - return
; + return data={rows} columns={columns} />; }; diff --git a/ui/blocks/src/ComponentDependencies/LocalDependencies.tsx b/ui/blocks/src/ComponentDependencies/LocalDependencies.tsx index 96cc1fe38..df085cc22 100644 --- a/ui/blocks/src/ComponentDependencies/LocalDependencies.tsx +++ b/ui/blocks/src/ComponentDependencies/LocalDependencies.tsx @@ -3,16 +3,17 @@ import { FC, useMemo } from 'react'; import { jsx, Flex, Text } from 'theme-ui'; import { Component, JSXNode } from '@component-controls/core'; -import { Table } from '@component-controls/components'; +import { Table, Column } from '@component-controls/components'; import { LocalImport } from '../PackageLink'; export interface LocalDependenciesProps { component?: Component; } -type TableImportType = { +type TableImportTypeRow = { from: string; imports: Omit[]; -}[]; +}; +type TableImportType = TableImportTypeRow[]; /** * base component dependencies @@ -37,40 +38,45 @@ export const LocalDependencies: FC = ({ : []; }, [component]); const columns = useMemo( - () => [ - { - Header: 'file', - accessor: 'name', - Cell: ({ - row: { - original: { from }, - }, - }: any) => ( - {`"${from}"`} - ), - }, - { - Header: 'imports', - accessor: 'imports', - Cell: ({ value }: { value: JSXNode[] }) => ( - - {value.map(node => ( - - ))} - - ), - }, - ], + () => + [ + { + Header: 'file', + accessor: 'name', + Cell: ({ + row: { + original: { from }, + }, + }: any) => ( + {`"${from}"`} + ), + }, + { + Header: 'imports', + accessor: 'imports', + Cell: ({ value }: { value: JSXNode[] }) => ( + + {value.map(node => ( + + ))} + + ), + }, + ] as Column[], [], ); @@ -79,6 +85,10 @@ export const LocalDependencies: FC = ({ } return ( -
+ + data-testid="local-dependencies" + data={imports} + columns={columns} + /> ); }; diff --git a/ui/blocks/src/ComponentJSX/ComponentJSX.tsx b/ui/blocks/src/ComponentJSX/ComponentJSX.tsx index febeaf7c6..1b85bbe0c 100644 --- a/ui/blocks/src/ComponentJSX/ComponentJSX.tsx +++ b/ui/blocks/src/ComponentJSX/ComponentJSX.tsx @@ -20,7 +20,7 @@ const NAME = 'component_jsx'; export const ComponentJSX: FC = fullProps => { const props = useCustomProps(NAME, fullProps); const component = useComponent({ of: props.of }); - if (!component?.jsx) { + if (!component?.jsx?.length) { return null; } return ( diff --git a/ui/blocks/src/ComponentJSX/ComponentJSXTree.tsx b/ui/blocks/src/ComponentJSX/ComponentJSXTree.tsx index b370b8307..b1afe2207 100644 --- a/ui/blocks/src/ComponentJSX/ComponentJSXTree.tsx +++ b/ui/blocks/src/ComponentJSX/ComponentJSXTree.tsx @@ -85,7 +85,7 @@ export const ComponentJSXTree: FC = ({ component }) => { data: { total, canExpand: expanded < total, - canCollapse: expanded > 0 && expanded < total, + canCollapse: expanded > 0 && expanded <= total, }, }); }; diff --git a/ui/blocks/src/ComponentJSX/ImportLabel.tsx b/ui/blocks/src/ComponentJSX/ImportLabel.tsx index 18251c223..1e03496b8 100644 --- a/ui/blocks/src/ComponentJSX/ImportLabel.tsx +++ b/ui/blocks/src/ComponentJSX/ImportLabel.tsx @@ -8,7 +8,10 @@ import { LocalImport } from '../PackageLink'; export const ImportLabel: FC<{ node: JSXNode }> = ({ node }) => { return ( - + {node.from && ( = ({ node }) => { +export const LocalImport: FC = ({ componentHash, name }) => { const store = useStore(); - const { componentKey, importedName, name } = node; const storypath = useMemo(() => { let docId = - componentKey && + componentHash && Object.keys(store.docs).find(id => { const doc = store.docs[id]; return doc?.componentsLookup - ? Object.values(doc?.componentsLookup).includes(componentKey) + ? Object.values(doc?.componentsLookup).includes(componentHash) : false; }); if (!docId) { @@ -42,8 +42,8 @@ export const LocalImport: FC = ({ node }) => { return (storyId || doc) && getStoryPath(storyId, doc, store); } return undefined; - }, [store, componentKey, name]); - const displayName = importedName === defaultExport ? name : importedName; + }, [store, componentHash, name]); + const displayName = name; return displayName ? ( + Pick, 'groupBy' | 'hiddenColumns' | 'expanded'> >; export const BasePropsTable: FC = ({ component = {}, @@ -149,10 +152,10 @@ export const BasePropsTable: FC = ({ } else { groupProps.hiddenColumns = ['prop.parentName']; } - const columns = [ + const columns: Column[] = [ { Header: 'Parent', - accessor: 'prop.parentName', + accessor: 'prop.parentName' as any, }, { Header: 'Name', @@ -185,7 +188,7 @@ export const BasePropsTable: FC = ({ }, { Header: 'Description', - accessor: 'prop.description', + accessor: 'prop.description' as any, Cell: ({ row: { original } }: any) => { if (!original) { return null; @@ -225,7 +228,7 @@ export const BasePropsTable: FC = ({ }, { Header: 'Default', - accessor: 'prop.defaultValue', + accessor: 'prop.defaultValue' as any, Cell: ({ row: { original } }: any) => { if (!original) { return null; @@ -251,7 +254,7 @@ export const BasePropsTable: FC = ({ ); }, }, - ...extraColumns, + ...(extraColumns as Column[]), ]; if (hasControls) { columns.push({ diff --git a/ui/blocks/src/PropsTable/PropsTable.tsx b/ui/blocks/src/PropsTable/PropsTable.tsx index 63662569c..a464063b3 100644 --- a/ui/blocks/src/PropsTable/PropsTable.tsx +++ b/ui/blocks/src/PropsTable/PropsTable.tsx @@ -3,7 +3,6 @@ import { jsx } from 'theme-ui'; import { FC } from 'react'; import { Column } from 'react-table'; -import { TableProps } from '@component-controls/components'; import { StoryContextProvider, ControlsContextStoryProvider, @@ -26,8 +25,7 @@ export interface PropsTableOwnProps { flat?: boolean; } export type PropsTableProps = PropsTableOwnProps & - Omit & - Omit; + Omit; const NAME = 'propstable'; diff --git a/ui/blocks/src/component-stats.mdx b/ui/blocks/src/component-stats.mdx new file mode 100644 index 000000000..f8c127170 --- /dev/null +++ b/ui/blocks/src/component-stats.mdx @@ -0,0 +1,34 @@ +--- +title: Blocks/internal usage +--- +import { ComponentUsage, AttributeUsage, ComponentUsageList, AttributesUsageList } from '@component-controls/addon-stats'; + +export const filter = ({ doc, component}) => component && doc.title.startsWith('Blocks'); + +# Blocks JSX stats + +This is an analysis of the `@component-controls/blocks` jsx components that have 'stories' + +## Components usage summary + +Components usage - how many times a component is being used from another component and which of their properties are used + + + +## Attributes usage summary + +Attributes usage - how many times an attribute is being set on a component, and on which component it is being set + + + +## Components usage details + +How many times a component is being used from another component, with a list of the components using it + + + +## Attributes usage details + +How many times an attribute is being used on a component, with a list of those components + + diff --git a/ui/components/src/ProgressIndicator/ProgressIndicator.stories.tsx b/ui/components/src/ProgressIndicator/ProgressIndicator.stories.tsx new file mode 100644 index 000000000..7eb0de04c --- /dev/null +++ b/ui/components/src/ProgressIndicator/ProgressIndicator.stories.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Document, Example, ControlTypes } from '@component-controls/core'; +import { ProgressIndicator, ProgressIndicatorProps } from './ProgressIndicator'; + +export default { + title: 'Components/ProgressIndicator', + component: ProgressIndicator, +} as Document; + +export const overview: Example = ({ + max, + value, + color, +}) => { + return ; +}; + +overview.controls = { + max: 10, + value: 3, + color: { type: ControlTypes.COLOR, value: 'red' }, +}; diff --git a/ui/components/src/ProgressIndicator/ProgressIndicator.tsx b/ui/components/src/ProgressIndicator/ProgressIndicator.tsx new file mode 100644 index 000000000..9eb770ef1 --- /dev/null +++ b/ui/components/src/ProgressIndicator/ProgressIndicator.tsx @@ -0,0 +1,25 @@ +/** @jsx jsx */ +import { FC } from 'react'; +import { jsx, Box, Text, Progress } from 'theme-ui'; + +export interface ProgressIndicatorProps { + value: number; + max: number; + color?: string; +} +export const ProgressIndicator: FC = ({ + value, + max, + color, +}) => ( + + + {value} + +); diff --git a/ui/components/src/ProgressIndicator/index.ts b/ui/components/src/ProgressIndicator/index.ts new file mode 100644 index 000000000..224938fda --- /dev/null +++ b/ui/components/src/ProgressIndicator/index.ts @@ -0,0 +1 @@ +export * from './ProgressIndicator'; diff --git a/ui/components/src/Table/Table.stories.tsx b/ui/components/src/Table/Table.stories.tsx index 8289cf980..0ea31bc71 100644 --- a/ui/components/src/Table/Table.stories.tsx +++ b/ui/components/src/Table/Table.stories.tsx @@ -1,7 +1,7 @@ /* eslint-disable react/display-name */ import React, { useMemo, useState, useEffect } from 'react'; import { Document, Example, faker } from '@component-controls/core'; -import { Table } from './Table'; +import { Table, Column } from './Table'; import { ThemeProvider } from '../ThemeContext'; export default { @@ -9,7 +9,17 @@ export default { component: Table, } as Document; -const columns = [ +type DataType = { + age: number; + name: string; + username: string; + address: { + street: string; + city: string; + zipcode: string; + }; +}; +const columns: Column[] = [ { Header: 'Age', accessor: 'age', @@ -25,18 +35,18 @@ const columns = [ }, { Header: 'Street', - accessor: 'address.street', + accessor: 'address.street' as any, }, { Header: 'City', - accessor: 'address.city', + accessor: 'address.city' as any, }, { Header: 'Zip Code', - accessor: 'address.zipcode', + accessor: 'address.zipcode' as any, }, ]; -const mockData = () => { +const mockData = (): DataType[] => { let i = 10; faker.seed(123); // eslint-disable-next-line prefer-spread @@ -52,7 +62,7 @@ export const overview: Example = () => { const data = useMemo(mockData, []); return ( -
+ hiddenColumns={['age']} columns={columns} data={data} /> ); }; @@ -61,7 +71,7 @@ export const noHeader: Example = () => { const data = useMemo(mockData, []); return ( -
header={false} hiddenColumns={['age']} columns={columns} @@ -74,7 +84,7 @@ export const sortable: Example = () => { const data = useMemo(mockData, []); return ( -
sorting={true} columns={columns} data={data} @@ -102,7 +112,7 @@ export const grouping: Example = () => { const data = useMemo(mockData, []); return ( -
expanded={{ 'age:21': true }} groupBy={['age']} columns={columns} @@ -119,29 +129,34 @@ export const editing: Example = () => { setSkipPageReset(false); }, [data]); const columns = useMemo( - () => [ - { - Header: 'Value', - accessor: 'value', - Cell: ({ cell: { value } }: any) => { - return ( - { - setSkipPageReset(true); - setData([{ value: e.target.value }]); - }} - /> - ); + () => + [ + { + Header: 'Value', + accessor: 'value', + Cell: ({ cell: { value } }: any) => { + return ( + { + setSkipPageReset(true); + setData([{ value: e.target.value }]); + }} + /> + ); + }, }, - }, - ], + ] as Column<{ value: string }>[], [], ); return ( -
+ + skipPageReset={skipPageReset} + columns={columns} + data={data} + /> ); }; @@ -150,7 +165,7 @@ export const rowSelect: Example = () => { const data = useMemo(mockData, []); return ( -
+ rowSelect={true} columns={columns} data={data} /> ); }; diff --git a/ui/components/src/Table/Table.tsx b/ui/components/src/Table/Table.tsx index c30519868..1d111f526 100644 --- a/ui/components/src/Table/Table.tsx +++ b/ui/components/src/Table/Table.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/jsx-key */ /** @jsx jsx */ -import { FC, Fragment, ReactNode, useEffect } from 'react'; +import { Fragment, ReactNode, ReactElement, useEffect } from 'react'; import { Box, BoxProps, Flex, jsx } from 'theme-ui'; import memoize from 'fast-memoize'; import { @@ -13,6 +13,7 @@ import { Column, Cell, Row, + PluginHook, TableOptions, UseFiltersOptions, UseExpandedOptions, @@ -40,16 +41,17 @@ const defaultColumn = memoize(() => ({ accessor: '', })); +export { Column, Cell, Row }; export type SelectedRowIds = Record; -interface TableOwnProps { +interface TableOwnProps { /** * the columns object as an array. */ - columns: Column>[]; + columns: Column[]; /** * array of data rows. */ - data?: any[]; + data?: D[]; /** * show or hide the header element. */ @@ -112,13 +114,14 @@ interface TableOwnProps { sortBy?: Array>; } -export type TableProps = TableOwnProps & BoxProps; +export type TableProps = TableOwnProps & + BoxProps; /** * Table component. Uses [react-table](https://github.com/tannerlinsley/react-table) to display the data. * Can be grouped, filtered, sorted. Themed with theme-ui for consistency. */ -export const Table: FC = ({ +export function Table({ columns, data = [], header = true, @@ -135,8 +138,8 @@ export const Table: FC = ({ rowSelect, sortBy, ...rest -}) => { - const plugins = [ +}: TableProps): ReactElement | null { + const plugins: PluginHook[] = [ useTableLayout, useGlobalFilter, useGroupBy, @@ -148,11 +151,11 @@ export const Table: FC = ({ if (rowSelect) { plugins.push(useRowSelectionColumn); } - const initialState: Partial>> & - Partial>> & - Partial>> & - Partial>> & - Partial>> = {}; + const initialState: Partial> & + Partial>> & + Partial> & + Partial> & + Partial>> = {}; if (Array.isArray(groupBy)) { initialState.groupBy = groupBy; initialState.hiddenColumns = hiddenColumns || groupBy; @@ -166,17 +169,17 @@ export const Table: FC = ({ initialState.expanded = expanded; } initialState.selectedRowIds = initialSelected; - const options: TableOptions> & - UseFiltersOptions> & - UseExpandedOptions> & - UsePaginationOptions> & - UseGroupByOptions> & - UseRowSelectOptions> & - UseSortByOptions> & - UseRowStateOptions> = { + const options: TableOptions & + UseFiltersOptions & + UseExpandedOptions & + UsePaginationOptions & + UseGroupByOptions & + UseRowSelectOptions & + UseSortByOptions & + UseRowStateOptions = { columns, data, - defaultColumn: defaultColumn() as Column>, + defaultColumn: defaultColumn() as Column, initialState, autoResetPage: !skipPageReset, autoResetExpanded: !skipPageReset, @@ -187,7 +190,7 @@ export const Table: FC = ({ autoResetRowState: !skipPageReset, }; - const tableOptions = useTable(options, ...plugins) as any; + const tableOptions = useTable(options, ...plugins) as any; const { getTableProps, getTableBodyProps, @@ -265,7 +268,7 @@ export const Table: FC = ({ {rows.map( ( row: Row & - UseGroupByRowProps> & { + UseGroupByRowProps & { isExpanded?: boolean; }, ) => { @@ -277,12 +280,7 @@ export const Table: FC = ({ {row.isGrouped ? row.cells[0].render('Aggregated') : row.cells.map( - ( - cell: Cell & - Partial< - UseGroupByCellProps> - >, - ) => { + (cell: Cell & Partial>) => { return ( = ({ ); -}; +} diff --git a/ui/components/src/Table/TableGrouping.tsx b/ui/components/src/Table/TableGrouping.tsx index 62ef94405..07a573192 100644 --- a/ui/components/src/Table/TableGrouping.tsx +++ b/ui/components/src/Table/TableGrouping.tsx @@ -28,9 +28,9 @@ const useControlledState = (state: GroupByState) => { return state; }, [state]); }; -export const useExpanderColumn = (itemsLabel: string) => ( - hooks: UseTableHooks>, -): void => { +export const useExpanderColumn = >( + itemsLabel: string, +) => (hooks: UseTableHooks): void => { hooks.useControlledState.push(useControlledState); hooks.visibleColumns.push((columns, { instance }) => { if ( @@ -48,8 +48,8 @@ export const useExpanderColumn = (itemsLabel: string) => ( Cell: ({ row, }: { - row: UseExpandedRowProps> & - UseTableRowProps> & { + row: UseExpandedRowProps & + UseTableRowProps & { groupByVal: any; }; }) => { diff --git a/ui/components/src/Table/TableRowSelection.tsx b/ui/components/src/Table/TableRowSelection.tsx index e9da49c44..cb7e72c6f 100644 --- a/ui/components/src/Table/TableRowSelection.tsx +++ b/ui/components/src/Table/TableRowSelection.tsx @@ -19,8 +19,8 @@ const IndeterminateCheckbox: FC = forwardRef( ); }, ); -export const useRowSelectionColumn = ( - hooks: UseTableHooks>, +export const useRowSelectionColumn = >( + hooks: UseTableHooks, ): void => { hooks.visibleColumns.push(columns => [ { @@ -28,12 +28,12 @@ export const useRowSelectionColumn = ( width: 30, Header: ({ getToggleAllRowsSelectedProps, - }: UseRowSelectInstanceProps<{}>) => ( + }: UseRowSelectInstanceProps) => ( ), // The cell can use the individual row's getToggleRowSelectedProps method // to the render a checkbox - Cell: ({ row }: { row: UseRowSelectRowProps<{}> }) => ( + Cell: ({ row }: { row: UseRowSelectRowProps }) => (
diff --git a/ui/components/src/Table/useTableLayout.ts b/ui/components/src/Table/useTableLayout.ts index 1209fa461..b8103dfba 100644 --- a/ui/components/src/Table/useTableLayout.ts +++ b/ui/components/src/Table/useTableLayout.ts @@ -1,6 +1,8 @@ import { Hooks } from 'react-table'; -export function useTableLayout(hooks: Hooks): void { +export function useTableLayout>( + hooks: Hooks, +): void { hooks.getHeaderProps.push(getHeaderProps); hooks.getCellProps.push(getCellProps); } diff --git a/ui/components/src/component-stats.mdx b/ui/components/src/component-stats.mdx new file mode 100644 index 000000000..51be9b86b --- /dev/null +++ b/ui/components/src/component-stats.mdx @@ -0,0 +1,34 @@ +--- +title: Components/internal usage +--- +import { ComponentUsage, AttributeUsage, ComponentUsageList, AttributesUsageList } from '@component-controls/addon-stats'; + +export const filter = ({ doc, component}) => component && doc.title.startsWith('Components'); + +# Components JSX stats + +This is an analysis of the `@component-controls/components` jsx components that have 'stories' + +## Components usage summary + +Components usage - how many times a component is being used from another component and which of their properties are used + + + +## Attributes usage summary + +Attributes usage - how many times an attribute is being set on a component, and on which component it is being set + + + +## Components usage details + +How many times a component is being used from another component, with a list of the components using it + + + +## Attributes usage details + +How many times an attribute is being used on a component, with a list of those components + + diff --git a/ui/components/src/index.ts b/ui/components/src/index.ts index 3cd4914a9..a159abab4 100644 --- a/ui/components/src/index.ts +++ b/ui/components/src/index.ts @@ -25,6 +25,7 @@ export * from './Multiselect'; export * from './Pagination'; export * from './PanelContainer'; export * from './Popover'; +export * from './ProgressIndicator'; export * from './SearchInput'; export * from './Sidebar'; export * from './SkipLinks';