From 0204f50533cea5e2eeb694fe64cc6fc115fd2fd4 Mon Sep 17 00:00:00 2001 From: atanasster Date: Tue, 8 Dec 2020 02:22:31 -0500 Subject: [PATCH] feat: addon-stats --- .../instrument/src/babel/extract-component.ts | 5 +- examples/gatsby/.config/buildtime.js | 2 + examples/nextjs/.config/buildtime.js | 2 + .../stories/src/blogs/component-stats.mdx | 63 +++++ .../src/blogs/media/components-usage-blog.jpg | Bin 0 -> 57933 bytes examples/stories/src/components/Button.tsx | 2 +- plugins/addon-stats/.config/buildtime.js | 5 + plugins/addon-stats/.eslintignore | 1 + plugins/addon-stats/LICENSE.md | 21 ++ plugins/addon-stats/README.md | 195 +++++++++++++++ plugins/addon-stats/package.json | 59 +++++ plugins/addon-stats/rollup.config.js | 5 + plugins/addon-stats/src/api/components.ts | 232 ++++++++++++++++++ plugins/addon-stats/src/api/index.ts | 1 + plugins/addon-stats/src/hooks/components.ts | 38 +++ plugins/addon-stats/src/hooks/index.ts | 1 + plugins/addon-stats/src/index.ts | 4 + plugins/addon-stats/src/types.ts | 74 ++++++ .../src/ui/AttributeUsage/AttributeUsage.tsx | 125 ++++++++++ .../src/ui/AttributeUsage/index.ts | 1 + .../AttributesUsageDetails.tsx | 81 ++++++ .../src/ui/AttributesUsageDetails/index.ts | 1 + .../AttributesUsageList.tsx | 25 ++ .../src/ui/AttributesUsageList/index.ts | 1 + .../src/ui/ComponentUsage/ComponentUsage.tsx | 125 ++++++++++ .../src/ui/ComponentUsage/index.ts | 1 + .../ComponentUsageDetails.tsx | 78 ++++++ .../src/ui/ComponentUsageDetails/index.ts | 1 + .../ComponentUsageList/ComponentUsageList.tsx | 30 +++ .../src/ui/ComponentUsageList/index.ts | 1 + plugins/addon-stats/src/ui/index.ts | 6 + plugins/addon-stats/tsconfig.json | 15 ++ .../axe-plugin/src/AllyBlock/NodesTable.tsx | 14 +- .../axe-plugin/src/AllyBlock/ResultsTable.tsx | 12 +- plugins/viewport-plugin/README.md | 8 +- plugins/viewport-plugin/package.json | 2 +- .../ExternalDependencies.tsx | 106 ++++---- .../LocalDependencies.tsx | 86 ++++--- ui/blocks/src/ComponentJSX/ComponentJSX.tsx | 2 +- .../src/ComponentJSX/ComponentJSXTree.tsx | 2 +- ui/blocks/src/ComponentJSX/ImportLabel.tsx | 5 +- ui/blocks/src/PackageLink/LocalImport.tsx | 16 +- ui/blocks/src/PropsTable/BasePropsTable.tsx | 15 +- ui/blocks/src/PropsTable/PropsTable.tsx | 4 +- ui/blocks/src/component-stats.mdx | 34 +++ .../ProgressIndicator.stories.tsx | 22 ++ .../ProgressIndicator/ProgressIndicator.tsx | 25 ++ ui/components/src/ProgressIndicator/index.ts | 1 + ui/components/src/Table/Table.stories.tsx | 71 +++--- ui/components/src/Table/Table.tsx | 60 +++-- ui/components/src/Table/TableGrouping.tsx | 10 +- ui/components/src/Table/TableRowSelection.tsx | 8 +- ui/components/src/Table/useTableLayout.ts | 4 +- ui/components/src/component-stats.mdx | 34 +++ ui/components/src/index.ts | 1 + 55 files changed, 1550 insertions(+), 193 deletions(-) create mode 100644 examples/stories/src/blogs/component-stats.mdx create mode 100644 examples/stories/src/blogs/media/components-usage-blog.jpg create mode 100644 plugins/addon-stats/.config/buildtime.js create mode 100644 plugins/addon-stats/.eslintignore create mode 100644 plugins/addon-stats/LICENSE.md create mode 100644 plugins/addon-stats/README.md create mode 100644 plugins/addon-stats/package.json create mode 100644 plugins/addon-stats/rollup.config.js create mode 100644 plugins/addon-stats/src/api/components.ts create mode 100644 plugins/addon-stats/src/api/index.ts create mode 100644 plugins/addon-stats/src/hooks/components.ts create mode 100644 plugins/addon-stats/src/hooks/index.ts create mode 100644 plugins/addon-stats/src/index.ts create mode 100644 plugins/addon-stats/src/types.ts create mode 100644 plugins/addon-stats/src/ui/AttributeUsage/AttributeUsage.tsx create mode 100644 plugins/addon-stats/src/ui/AttributeUsage/index.ts create mode 100644 plugins/addon-stats/src/ui/AttributesUsageDetails/AttributesUsageDetails.tsx create mode 100644 plugins/addon-stats/src/ui/AttributesUsageDetails/index.ts create mode 100644 plugins/addon-stats/src/ui/AttributesUsageList/AttributesUsageList.tsx create mode 100644 plugins/addon-stats/src/ui/AttributesUsageList/index.ts create mode 100644 plugins/addon-stats/src/ui/ComponentUsage/ComponentUsage.tsx create mode 100644 plugins/addon-stats/src/ui/ComponentUsage/index.ts create mode 100644 plugins/addon-stats/src/ui/ComponentUsageDetails/ComponentUsageDetails.tsx create mode 100644 plugins/addon-stats/src/ui/ComponentUsageDetails/index.ts create mode 100644 plugins/addon-stats/src/ui/ComponentUsageList/ComponentUsageList.tsx create mode 100644 plugins/addon-stats/src/ui/ComponentUsageList/index.ts create mode 100644 plugins/addon-stats/src/ui/index.ts create mode 100644 plugins/addon-stats/tsconfig.json create mode 100644 ui/blocks/src/component-stats.mdx create mode 100644 ui/components/src/ProgressIndicator/ProgressIndicator.stories.tsx create mode 100644 ui/components/src/ProgressIndicator/ProgressIndicator.tsx create mode 100644 ui/components/src/ProgressIndicator/index.ts create mode 100644 ui/components/src/component-stats.mdx 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 0000000000000000000000000000000000000000..ff484b662b6e9baea29b3fd71f5078582ec43c94 GIT binary patch literal 57933 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 x^?maH@LfnMX!7rhef%=IDoOIF5rd3>u{ = ({ 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';