diff --git a/www/gatsby-node.js b/www/gatsby-node.js index 5653403cd5..525ea7fb38 100644 --- a/www/gatsby-node.js +++ b/www/gatsby-node.js @@ -4,193 +4,16 @@ * See: https://www.gatsbyjs.com/docs/node-apis/ */ -// You can delete this file if you're not using it -const path = require('path'); -const { createFilePath } = require('gatsby-source-filesystem'); -const sass = require('sass'); -const css = require('css'); +const createPages = require('./utils/createPages'); +const onCreateNode = require('./utils/onCreateNode'); +const onCreateWebpackConfig = require('./utils/onCreateWebpackConfig'); +const createCssUtilityClassNodes = require('./utils/createCssUtilityClassNodes'); -const fs = require('fs'); -const { INSIGHTS_PAGES } = require('./src/config'); -const { getThemesSCSSVariables, processComponentSCSSVariables } = require('./theme-utils'); +exports.onCreateWebpackConfig = ({ actions }) => onCreateWebpackConfig(actions); -exports.onCreateWebpackConfig = ({ actions }) => { - actions.setWebpackConfig({ - resolve: { - alias: { - '~paragon-react': path.resolve(__dirname, '../src'), - '~paragon-style': path.resolve(__dirname, '../scss'), - '~paragon-icons': path.resolve(__dirname, '../icons'), - }, - }, - }); -}; - -exports.onCreateNode = ({ node, actions, getNode }) => { - const { createNodeField } = actions; - // you only want to operate on `Mdx` nodes. If you had content from a - // remote CMS you could also check to see if the parent node was a - // `File` node here - if (node.internal.type === 'Mdx') { - const value = createFilePath({ node, getNode }) - .split('README')[0] - .toLowerCase(); - - const isChangelogNode = node.fileAbsolutePath && node.fileAbsolutePath.endsWith('CHANGELOG.md'); - - createNodeField({ - // Name of the field you are adding - name: 'slug', - // Individual MDX node - node, - // Generated value based on filepath with 'components' prefix. you - // don't need a separating '/' before the value because - // createFilePath returns a path with the leading '/'. - value: isChangelogNode ? 'changelog' : `/components${value}`, - }); - } -}; - -exports.createPages = async ({ graphql, actions, reporter }) => { - // Destructure the createPage function from the actions object - const { createPage, createRedirect } = actions; - // MDX transforms markdown generated by gatsby-transformer-react-docgen - // This query filters out all of those markdown nodes and assumes all others - // are for page creation purposes. - const result = await graphql(` - query { - allMdx( - filter: { - parent: { - internal: { owner: { nin: "gatsby-transformer-react-docgen" } } - } - } - ) { - edges { - node { - id - fields { - slug - } - frontmatter { - components - } - slug - } - } - } - } - `); - if (result.errors) { - reporter.panicOnBuild('🚨 ERROR: Loading createPages query'); - } - // Create component detail pages. - const components = result.data.allMdx.edges; - - const themesSCSSVariables = await getThemesSCSSVariables(); - - // you'll call `createPage` for each result - // eslint-disable-next-line no-restricted-syntax - for (const { node } of components) { - const componentDir = node.slug.split('/')[0]; - const variablesPath = path.resolve(__dirname, `../src/${componentDir}/_variables.scss`); - let scssVariablesData = {}; - - if (fs.existsSync(variablesPath)) { - // eslint-disable-next-line no-await-in-loop - scssVariablesData = await processComponentSCSSVariables(variablesPath, themesSCSSVariables); - } - - createPage({ - // This is the slug you created before - // (or `node.frontmatter.slug`) - path: node.fields.slug, - // This component will wrap our MDX content - component: path.resolve('./src/templates/component-page-template.tsx'), - // You can use the values in this context in - // our page layout component - context: { id: node.id, components: node.frontmatter.components || [], scssVariablesData }, - }); - } - - INSIGHTS_PAGES.forEach(({ path: pagePath, tab }) => { - createPage({ - path: pagePath, - component: require.resolve('./src/pages/insights.tsx'), - context: { tab }, - }); - }); - - createRedirect({ - fromPath: '/playroom', - toPath: '/playroom/index.html', - }); - - createRedirect({ - fromPath: '/playroom/preview', - toPath: '/playroom/preview/index.html', - }); -}; - -function createCssUtilityClassNodes({ - actions, - createNodeId, - createContentDigest, -}) { - const { createNode } = actions; - - // We convert to CSS first since we prefer the real values over tokens. - const compiledCSS = sass - .renderSync({ - file: path.resolve(__dirname, '../scss/core/utilities-only.scss'), - // Resolve tildes the way webpack would in our base npm project - importer(url) { - let resolvedUrl = url; - if (url[0] === '~') { - resolvedUrl = path.resolve(__dirname, '../node_modules', url.substr(1)); - } - return { file: resolvedUrl }; - }, - }) - .css.toString(); - - const sheet = css.parse(compiledCSS).stylesheet; - - sheet.rules.forEach(({ selectors, position, declarations }) => { - if (!selectors) { return; } - - selectors.forEach(selector => { - if (selector[0] !== '.') { return; } // classes only - - const classSelector = selector.substr(1); - - const nodeData = { - selector: classSelector, - declarations: declarations.map( - ({ property, value }) => `${property}: ${value};`, - ), - isUtility: - declarations.length === 1 - && declarations[0].value.includes('!important'), - }; - - const nodeMeta = { - id: createNodeId( - `rule-${classSelector}-${position.start.line}-${position.end.line}`, - ), - parent: null, - children: [], - internal: { - type: 'CssUtilityClasses', - contentDigest: createContentDigest(nodeData), - }, - }; +exports.onCreateNode = ({ node, actions, getNode }) => onCreateNode(node, actions, getNode); - const node = { ...nodeData, ...nodeMeta }; - createNode(node); - }); - }); -} +exports.createPages = ({ graphql, actions, reporter }) => createPages(graphql, actions, reporter); exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => { createCssUtilityClassNodes({ actions, createNodeId, createContentDigest }); diff --git a/www/src/components/insights/ComponentUsage.tsx b/www/src/components/insights/ComponentUsage.tsx new file mode 100644 index 0000000000..961cf910c9 --- /dev/null +++ b/www/src/components/insights/ComponentUsage.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { DataTable } from '~paragon-react'; +import ComponentUsageExamples, { IComponentUsageExamples } from './ComponentUsageExamples'; + +import { IComponentUsage } from '../../types/types'; + +function ComponentUsage({ name, componentUsageInProjects }: IComponentUsage) { + return ( +
+

{name}

+ ( + + )} + columns={[ + { + id: 'expander', + Header: DataTable.ExpandAll, + Cell: DataTable.ExpandRow, + }, + { + Header: 'Project Name', + accessor: 'folderName', + }, + { Header: 'Paragon Version', accessor: 'version' }, + { Header: 'Instance Count', accessor: 'componentUsageCount' }, + ]} + > + + + +
+ ); +} + +ComponentUsage.propTypes = { + name: PropTypes.string.isRequired, + componentUsageInProjects: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string, + folderName: PropTypes.string, + version: PropTypes.string, + repositoryUrl: PropTypes.string, + componentUsageCount: PropTypes.number, + usages: PropTypes.arrayOf(PropTypes.shape({ + column: PropTypes.number, + filePath: PropTypes.string, + line: PropTypes.number, + version: PropTypes.string, + })), + })).isRequired, +}; + +export default ComponentUsage; diff --git a/www/src/components/insights/ComponentsUsage.tsx b/www/src/components/insights/ComponentsUsage.tsx new file mode 100644 index 0000000000..08bd4df561 --- /dev/null +++ b/www/src/components/insights/ComponentsUsage.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import ComponentUsage from './ComponentUsage'; + +import componentsUsage from '../../utils/componentsUsage'; +import getEmptyMessage from '../../utils/getEmptyMessage'; +import usagePropTypes from '../../utils/usagePropTypes'; +import removeDotsFromKeys from '../../utils/removeDotsFromKey'; + +function ComponentsUsage({ data }: { data: string[] }) { + const filteredComponentsUsage = removeDotsFromKeys(componentsUsage); + + return ( +
+ {data.length ? data.sort().map(name => { + if (filteredComponentsUsage[name]) { + return ( + + ); + } + return null; + }) : getEmptyMessage('components')} +
+ ); +} + +ComponentsUsage.propTypes = usagePropTypes; + +export default ComponentsUsage; diff --git a/www/src/components/insights/HooksUsage.tsx b/www/src/components/insights/HooksUsage.tsx new file mode 100644 index 0000000000..1094708ffd --- /dev/null +++ b/www/src/components/insights/HooksUsage.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import ComponentUsage from './ComponentUsage'; + +import componentsUsage from '../../utils/componentsUsage'; +import getEmptyMessage from '../../utils/getEmptyMessage'; +import usagePropTypes from '../../utils/usagePropTypes'; + +function HooksUsage({ data }: { data: string[] }) { + return ( +
+ {data.length ? data.sort().map(name => ( + + )) : getEmptyMessage('hooks')} +
+ ); +} + +HooksUsage.propTypes = usagePropTypes; + +export default HooksUsage; diff --git a/www/src/components/insights/IconsUsage.tsx b/www/src/components/insights/IconsUsage.tsx new file mode 100644 index 0000000000..f0fdf601ac --- /dev/null +++ b/www/src/components/insights/IconsUsage.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import ComponentUsage from './ComponentUsage'; + +import componentsUsage from '../../utils/componentsUsage'; +import getEmptyMessage from '../../utils/getEmptyMessage'; +import usagePropTypes from '../../utils/usagePropTypes'; + +function IconsUsage({ data }: { data: string[] }) { + return ( +
+ {data.length ? data.sort().map(name => ( + + )) : getEmptyMessage('utils')} +
+ ); +} + +IconsUsage.propTypes = usagePropTypes; + +export default IconsUsage; diff --git a/www/src/components/insights/ProjectsUsage.tsx b/www/src/components/insights/ProjectsUsage.tsx new file mode 100644 index 0000000000..ba53716fa0 --- /dev/null +++ b/www/src/components/insights/ProjectsUsage.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { DataTable } from '~paragon-react'; +import ProjectUsageExamples, { IProjectUsageExamples } from './ProjectUsageExamples'; + +import getDependentProjectsUsages from '../../utils/getDependentProjectsUsages'; + +function ProjectsUsage() { + const dependentProjects = getDependentProjectsUsages(); + + return ( +
+

Projects in Open edX consuming Paragon

+ } + columns={[ + { + id: 'expander', + Header: DataTable.ExpandAll, + Cell: DataTable.ExpandRow, + }, + { + Header: 'Project Name', + accessor: 'folderName', + }, + { Header: 'Paragon Version', accessor: 'version' }, + { Header: 'Import Count', accessor: 'count' }, + ]} + > + + + + + +
+ ); +} + +export default ProjectsUsage; diff --git a/www/src/components/insights/SummaryUsage.tsx b/www/src/components/insights/SummaryUsage.tsx new file mode 100644 index 0000000000..3d9127280b --- /dev/null +++ b/www/src/components/insights/SummaryUsage.tsx @@ -0,0 +1,130 @@ +import React, { useContext } from 'react'; +import { + DataTable, + TextFilter, + CheckboxFilter, + useMediaQuery, + breakpoints, +} from '~paragon-react'; +import componentsUsage from '../../utils/componentsUsage'; +import InsightsContext from '../../context/InsightsContext'; +import SummaryUsageExamples, { ISummaryUsageExamples } from './SummaryUsageExamples'; +import { IComponentUsageData, IInsightsContext } from '../../types/types'; +import getDependentProjectsUsages from '../../utils/getDependentProjectsUsages'; + +interface IFilterData { + name: string, + number: number | undefined, + value: string +} + +const round = (n: number) => Math.round(n * 10) / 10; + +const ICON_TYPE = 'Icon'; +const TABLE_PAGE_SIZE = 10; +const componentsInUsage = Object.keys(componentsUsage); +const dependentProjects = getDependentProjectsUsages(); + +function SummaryUsage() { + const { paragonTypes = {}, isParagonIcon = () => false } = useContext(InsightsContext) as IInsightsContext; + const isMedium = useMediaQuery({ minWidth: breakpoints.large.minWidth }); + + const typeCount = Object.keys(paragonTypes) + .reduce((accumulator: { [key: string]: number | undefined }, componentName) => { + const type = paragonTypes[componentName] || (isParagonIcon(componentName) && ICON_TYPE); + if (componentsInUsage.includes(componentName)) { + accumulator[type] = (accumulator[type] || 0) + 1; + } + return accumulator; + }, {}); + + const filterValues: IFilterData[] = Object.keys(paragonTypes) + .map((key) => paragonTypes[key]) + .filter((v, i, a) => a.indexOf(v) === i) + .map(type => ({ name: type, number: typeCount[type], value: type })); + // Number of Icons is calculated in the statement below. Initialized as `undefined` to not display '0'. + const iconsType: IFilterData = { name: ICON_TYPE, number: undefined, value: ICON_TYPE }; + + const summaryComponentsUsage = Object.entries(componentsUsage).map( + ([componentName, usages]) => { + const componentUsageCounts = usages + .reduce((accumulator, project) => accumulator + project.componentUsageCount, 0); + let type = paragonTypes[componentName]; + if (!type && isParagonIcon(componentName)) { + type = ICON_TYPE; + iconsType.number = (iconsType.number || 0) + 1; + } + return { + name: componentName, + count: componentUsageCounts, + usages: componentsUsage[componentName], + type, + }; + }, + ); + filterValues.push(iconsType); + typeCount[ICON_TYPE] = iconsType.number; + + const summaryTableData = summaryComponentsUsage.sort((a, b) => { + const nameA = a.name.toUpperCase(); + const nameB = b.name.toUpperCase(); + return nameA < nameB ? -1 : 1; + }); + + const averageComponentsUsedPerProject = dependentProjects + .reduce((accumulator, project) => accumulator + project.count, 0) / dependentProjects.length; + return ( +
+
+

Overview

+

+ Paragon is used by at least {dependentProjects.length} projects, with an average + of {round(averageComponentsUsedPerProject)} imports per project. +

+
+

Overall usage

+ } + initialState={{ pageSize: TABLE_PAGE_SIZE }} + columns={[ + { + id: 'expander', + Header: DataTable.ExpandAll, + Cell: DataTable.ExpandRow, + }, + { + Header: 'Component Name', + accessor: 'name', + }, + { + Header: 'Instance Count', + accessor: 'count', + disableFilters: true, + }, + { + Header: 'Type', + accessor: 'type', + Filter: CheckboxFilter, + filter: 'includesValue', + filterChoices: filterValues, + }, + ]} + > + + + + + +
+ ); +} + +export default SummaryUsage; diff --git a/www/src/components/insights/UtilsUsage.tsx b/www/src/components/insights/UtilsUsage.tsx new file mode 100644 index 0000000000..8f91af5aad --- /dev/null +++ b/www/src/components/insights/UtilsUsage.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import ComponentUsage from './ComponentUsage'; + +import componentsUsage from '../../utils/componentsUsage'; +import getEmptyMessage from '../../utils/getEmptyMessage'; +import usagePropTypes from '../../utils/usagePropTypes'; + +function UtilsUsage({ data }: { data: string[] }) { + return ( +
+ {data.length ? data.sort().map(name => ( + + )) : getEmptyMessage('utils')} +
+ ); +} + +UtilsUsage.propTypes = usagePropTypes; + +export default UtilsUsage; diff --git a/www/src/context/InsightsContext.jsx b/www/src/context/InsightsContext.jsx index 75a014fb9f..d9d5f9f2f0 100644 --- a/www/src/context/InsightsContext.jsx +++ b/www/src/context/InsightsContext.jsx @@ -12,10 +12,9 @@ function InsightsContextProvider({ children }) { const [paragonTypes, setParagonTypes] = useState({}); useEffect(() => { - setParagonTypes(getParagonComponentsTypes(Components)); + setParagonTypes(getParagonComponentsTypes({ paragon: 'Paragon', ...Components })); }, []); - - const isParagonIcon = (name) => name in Icons; + const isParagonIcon = (name) => name in Icons || name.match('Icon'); const contextValue = useMemo(() => ({ paragonTypes, diff --git a/www/src/pages/insights.tsx b/www/src/pages/insights.tsx index 13bfb09fba..4781e12791 100644 --- a/www/src/pages/insights.tsx +++ b/www/src/pages/insights.tsx @@ -2,75 +2,30 @@ import React, { useContext } from 'react'; import { navigate } from 'gatsby'; import PropTypes from 'prop-types'; import { - DataTable, Tabs, Tab, Container, - TextFilter, - CheckboxFilter, - useMediaQuery, - breakpoints, } from '~paragon-react'; import SEO from '../components/SEO'; import Layout from '../components/PageLayout'; -import SummaryUsageExamples, { ISummaryUsageExamples } from '../components/insights/SummaryUsageExamples'; -import ProjectUsageExamples, { IProjectUsageExamples } from '../components/insights/ProjectUsageExamples'; -import ComponentUsageExamples, { IComponentUsageExamples } from '../components/insights/ComponentUsageExamples'; -import getGithubProjectUrl from '../utils/getGithubProjectUrl'; +import InsightsContext from '../context/InsightsContext'; +import SummaryUsage from '../components/insights/SummaryUsage'; +import ProjectsUsage from '../components/insights/ProjectsUsage'; +import HooksUsage from '../components/insights/HooksUsage'; +import UtilsUsage from '../components/insights/UtilsUsage'; +import IconsUsage from '../components/insights/IconsUsage'; +import ComponentsUsage from '../components/insights/ComponentsUsage'; + // @ts-ignore import dependentProjectsAnalysis from '../../../dependent-usage.json'; // eslint-disable-line import { INSIGHTS_TABS, INSIGHTS_PAGES } from '../config'; -import InsightsContext from '../context/InsightsContext'; - -const ICON_TYPE = 'Icon'; -const TABLE_PAGE_SIZE = 10; +import componentsUsage from '../utils/componentsUsage'; +import { IInsightsContext } from '../types/types'; const { lastModified: analysisLastUpdated, - projectUsages: dependentProjectsUsages, } = dependentProjectsAnalysis; -interface IUsage { - filePath: string, - line: number, - column: number, - version: string, -} - -interface IDependentUsage { - version?: string, - name?: string, - repository?: { type: string, url: string }, - repositoryUrl?: string, - count: number, - folderName?: string, - usages: { - [key: string]: IUsage[], - }, -} - -interface IComponentUsageData { - componentUsageCount: number, - folderName: string, - name: string, - repositoryUrl: string, - usages: IUsage[], - version: string, -} - -interface IInsightsContext { - paragonTypes: { - [key: string]: string - }, - isParagonIcon: Function, -} - -interface IFilterData { - name: string, - number: number | undefined, - value: string -} - interface TabsDataType { components: string[], hooks: string[], @@ -78,282 +33,6 @@ interface TabsDataType { icons: string[], } -export interface IComponentUsage { - name: string, - componentUsageInProjects: IComponentUsageData[], -} - -const dependentProjects: IDependentUsage[] = []; - -const componentsUsage: Record = dependentProjectsUsages - .reduce((accumulator: any, project: any) => { - dependentProjects.push({ - ...project, - repositoryUrl: getGithubProjectUrl(project.repository), - count: Object.values(project.usages).reduce((acc, usage) => acc + usage.length, 0), - }); - - Object.keys(project.usages).forEach(componentName => { - // The next line is necessary for the same naming of the components both in the file with the - // repositories of use and in the data structures GraphQL. - const newComponentName = componentName.replace(/\./g, ''); - if (!accumulator[newComponentName]) { - accumulator[newComponentName] = []; - } - accumulator[newComponentName] = accumulator[newComponentName].concat({ - name: project.name, - folderName: project.folderName, - version: project.version, - repositoryUrl: getGithubProjectUrl(project.repository), - componentUsageCount: project.usages[componentName].length, - usages: project.usages[componentName], - }); - }); - return accumulator; - }, {}); - -export const componentsInUsage = Object.keys(componentsUsage); - -const round = (n: number) => Math.round(n * 10) / 10; - -const getEmptyMessage = (text: string) => `Currently there are no ${text} usage yet`; - -function SummaryUsage() { - const { paragonTypes = {}, isParagonIcon = () => false } = useContext(InsightsContext) as IInsightsContext; - const isMedium = useMediaQuery({ minWidth: breakpoints.large.minWidth }); - - const typeCount = Object.keys(paragonTypes) - .reduce((accumulator: { [key: string]: number | undefined }, componentName) => { - const type = paragonTypes[componentName] || (isParagonIcon(componentName) && ICON_TYPE); - if (componentsInUsage.includes(componentName)) { - accumulator[type] = (accumulator[type] || 0) + 1; - } - return accumulator; - }, {}); - - const filterValues: IFilterData[] = Object.keys(paragonTypes) - .map((key) => paragonTypes[key]) - .filter((v, i, a) => a.indexOf(v) === i) - .map(type => ({ name: type, number: typeCount[type], value: type })); - // Number of Icons is calculated in the statement below. Initialized as `undefined` to not display '0'. - const iconsType: IFilterData = { name: ICON_TYPE, number: undefined, value: ICON_TYPE }; - - const summaryComponentsUsage = Object.entries(componentsUsage).map( - ([componentName, usages]) => { - const componentUsageCounts = usages - .reduce((accumulator, project) => accumulator + project.componentUsageCount, 0); - let type = paragonTypes[componentName]; - if (!type && isParagonIcon(componentName)) { - type = ICON_TYPE; - iconsType.number = (iconsType.number || 0) + 1; - } - return { - name: componentName, - count: componentUsageCounts, - usages: componentsUsage[componentName], - type, - }; - }, - ); - filterValues.push(iconsType); - typeCount[ICON_TYPE] = iconsType.number; - - const summaryTableData = summaryComponentsUsage.sort((a, b) => { - const nameA = a.name.toUpperCase(); - const nameB = b.name.toUpperCase(); - return nameA < nameB ? -1 : 1; - }); - - const averageComponentsUsedPerProject = dependentProjects - .reduce((accumulator, project) => accumulator + project.count, 0) / dependentProjects.length; - - return ( -
-
-

Overview

-

- Paragon is used by at least {dependentProjects.length} projects, with an average - of {round(averageComponentsUsedPerProject)} imports per project. -

-
-

Overall usage

- } - initialState={{ pageSize: TABLE_PAGE_SIZE }} - columns={[ - { - id: 'expander', - Header: DataTable.ExpandAll, - Cell: DataTable.ExpandRow, - }, - { - Header: 'Component Name', - accessor: 'name', - }, - { - Header: 'Instance Count', - accessor: 'count', - disableFilters: true, - }, - { - Header: 'Type', - accessor: 'type', - Filter: CheckboxFilter, - filter: 'includesValue', - filterChoices: filterValues, - }, - ]} - > - - - - - -
- ); -} - -// Paragon version in all projects -function ProjectsUsage() { - return ( -
-

Projects in Open edX consuming Paragon

- } - columns={[ - { - id: 'expander', - Header: DataTable.ExpandAll, - Cell: DataTable.ExpandRow, - }, - { - Header: 'Project Name', - accessor: 'folderName', - }, - { Header: 'Paragon Version', accessor: 'version' }, - { Header: 'Import Count', accessor: 'count' }, - ]} - > - - - - - -
- ); -} - -// Usage info about a single component -function ComponentUsage({ name, componentUsageInProjects }: IComponentUsage) { - return ( -
-

{name}

- ( - - )} - columns={[ - { - id: 'expander', - Header: DataTable.ExpandAll, - Cell: DataTable.ExpandRow, - }, - { - Header: 'Project Name', - accessor: 'folderName', - }, - { Header: 'Paragon Version', accessor: 'version' }, - { Header: 'Instance Count', accessor: 'componentUsageCount' }, - ]} - > - - - -
- ); -} - -// Usage info for all components -export function ComponentsUsage({ data }: { data: string[] }) { - return ( -
- {data.length ? data.sort().map(name => { - if (componentsUsage[name]) { - return ( - - ); - } - return null; - }) : getEmptyMessage('components')} -
- ); -} - -// Usage info for all hooks -function HooksUsage({ data }: { data: string[] }) { - return ( -
- {data.length ? data.sort().map(name => ( - - )) : getEmptyMessage('hooks')} -
- ); -} - -// Usage info for all utils -function UtilsUsage({ data }: { data: string[] }) { - return ( -
- {data.length ? data.sort().map(name => ( - - )) : getEmptyMessage('utils')} -
- ); -} - -// Usage info for all utils -function IconsUsage({ data }: { data: string[] }) { - return ( -
- {data.length ? data.sort().map(name => ( - - )) : getEmptyMessage('utils')} -
- ); -} - export default function InsightsPage({ pageContext: { tab } }: { pageContext: { tab: string } }) { const { paragonTypes = {}, isParagonIcon = () => false } = useContext(InsightsContext) as IInsightsContext; const { @@ -435,29 +114,3 @@ InsightsPage.propTypes = { tab: PropTypes.oneOf(Object.values(INSIGHTS_TABS)), }).isRequired, }; - -ComponentUsage.propTypes = { - name: PropTypes.string.isRequired, - componentUsageInProjects: PropTypes.arrayOf(PropTypes.shape({ - name: PropTypes.string, - folderName: PropTypes.string, - version: PropTypes.string, - repositoryUrl: PropTypes.string, - componentUsageCount: PropTypes.number, - usages: PropTypes.arrayOf(PropTypes.shape({ - column: PropTypes.number, - filePath: PropTypes.string, - line: PropTypes.number, - version: PropTypes.string, - })), - })).isRequired, -}; - -const usagePropTypes = { - data: PropTypes.arrayOf(PropTypes.string).isRequired, -}; - -ComponentsUsage.propTypes = usagePropTypes; -HooksUsage.propTypes = usagePropTypes; -UtilsUsage.propTypes = usagePropTypes; -IconsUsage.propTypes = usagePropTypes; diff --git a/www/src/templates/component-page-template.tsx b/www/src/templates/component-page-template.tsx index 933e4c04d1..8758f28568 100644 --- a/www/src/templates/component-page-template.tsx +++ b/www/src/templates/component-page-template.tsx @@ -17,7 +17,7 @@ import GenericPropsTable from '../components/PropsTable'; import Layout from '../components/PageLayout'; import SEO from '../components/SEO'; import LinkedHeading from '../components/LinkedHeading'; -import { componentsInUsage, ComponentsUsage } from '../pages/insights'; +import ComponentsUsage from '../components/insights/ComponentsUsage'; export interface IPageTemplate { data: { @@ -39,6 +39,7 @@ export interface IPageTemplate { }, pageContext: { scssVariablesData: Record, + componentsUsageInsights: string[], } } @@ -48,7 +49,7 @@ export type ShortCodesTypes = { export default function PageTemplate({ data: { mdx, components: componentNodes }, - pageContext: { scssVariablesData }, + pageContext: { scssVariablesData, componentsUsageInsights }, }: IPageTemplate) { const isMobile = useMediaQuery({ maxWidth: breakpoints.large.maxWidth }); const [showMinimizedTitle, setShowMinimizedTitle] = useState(false); @@ -94,6 +95,10 @@ export default function PageTemplate({ const usageInsightsTitle = 'Usage Insights'; const usageInsightsUrl = 'usage-insights'; + const sortedComponentNames = mdx.frontmatter?.components || []; + const filteredComponentsUsageInsights = componentsUsageInsights.map(componentName => componentName.replace(/\./g, '')); + const isUsageInsights = (sortedComponentNames as []).some(value => filteredComponentsUsageInsights.includes(value)); + const getTocData = () => { const tableOfContents = JSON.parse(JSON.stringify(mdx.tableOfContents)); if (Object.values(scssVariablesData).some(data => data) && !tableOfContents.items?.includes()) { @@ -103,32 +108,19 @@ export default function PageTemplate({ }); } tableOfContents.items?.push({ title: propsAPITitle, url: `#${propsAPIUrl}` }); - tableOfContents.items?.push({ title: usageInsightsTitle, url: `#${usageInsightsUrl}` }); + if (isUsageInsights) { + tableOfContents.items?.push({ + title: usageInsightsTitle, + url: `#${usageInsightsUrl}`, + }); + } return tableOfContents; }; - const sortedComponentNames = mdx.frontmatter?.components || []; - const isDeprecated = mdx.frontmatter?.status?.toLowerCase().includes('deprecate') || false; useEffect(() => setShowMinimizedTitle(!!isMobile), [isMobile]); - const usageComponents = {}; - - componentsInUsage.forEach(key => { - usageComponents[key] = null; - }); - - if (typeof sortedComponentNames !== 'string') { - sortedComponentNames.forEach(componentName => { - if (componentName in usageComponents) { - usageComponents[componentName] = componentName; - } - }); - } - - const noMatchingValues = (sortedComponentNames as []).every(componentName => !(componentName in usageComponents)); - return ( ; })} - {!noMatchingValues && ( + {isUsageInsights && ( <>

{usageInsightsTitle} diff --git a/www/src/types/types.ts b/www/src/types/types.ts new file mode 100644 index 0000000000..8e76d4c306 --- /dev/null +++ b/www/src/types/types.ts @@ -0,0 +1,46 @@ +export interface IInsightsContext { + paragonTypes: { + [key: string]: string + }, + isParagonIcon: Function, +} + +export interface IUsage { + filePath: string, + line: number, + column: number, + version: string, +} + +export interface IComponentUsageData { + componentUsageCount: number, + folderName: string, + name: string, + repositoryUrl: string, + usages: IUsage[], + version: string, +} + +export interface IDependentProjectsUsages extends Omit { + version: string, + name: string, + repository: { type: string, url: string }, + folderName: string, +} + +export interface IDependentUsage { + version?: string, + name?: string, + repository?: { type: string, url: string }, + repositoryUrl?: string, + count: number, + folderName?: string, + usages: { + [key: string]: IUsage[], + }, +} + +export interface IComponentUsage { + name: string, + componentUsageInProjects: IComponentUsageData[], +} diff --git a/www/src/utils/componentsUsage.js b/www/src/utils/componentsUsage.js new file mode 100644 index 0000000000..46388a37b6 --- /dev/null +++ b/www/src/utils/componentsUsage.js @@ -0,0 +1,27 @@ +const getGithubProjectUrl = require('./getGithubProjectUrl'); +const dependentProjectsAnalysis = require('../../../dependent-usage.json'); + +const { + projectUsages: dependentProjectsUsages, +} = dependentProjectsAnalysis; + +const componentsUsage = dependentProjectsUsages + .reduce((accumulator, project) => { + Object.keys(project.usages).forEach(componentName => { + if (!accumulator[componentName]) { + accumulator[componentName] = []; + } + accumulator[componentName] = accumulator[componentName].concat({ + name: project.name, + folderName: project.folderName, + version: project.version, + repositoryUrl: getGithubProjectUrl(project.repository), + componentUsageCount: project.usages[componentName].length, + usages: project.usages[componentName], + }); + }); + + return accumulator; + }, {}); + +module.exports = componentsUsage; diff --git a/www/src/utils/getDependentProjectsUsages.tsx b/www/src/utils/getDependentProjectsUsages.tsx new file mode 100644 index 0000000000..2657640f05 --- /dev/null +++ b/www/src/utils/getDependentProjectsUsages.tsx @@ -0,0 +1,22 @@ +// @ts-ignore +import dependentProjectsAnalysis from '../../../dependent-usage.json'; // eslint-disable-line +import getGithubProjectUrl from './getGithubProjectUrl'; +import { IDependentProjectsUsages, IDependentUsage, IUsage } from '../types/types'; + +const { + projectUsages: dependentProjectsUsages, +} = dependentProjectsAnalysis; + +export default function getDependentProjectsUsages() { + const dependentProjects: IDependentUsage[] = []; + + dependentProjectsUsages.forEach((project: IDependentProjectsUsages) => { + dependentProjects.push({ + ...project, + repositoryUrl: getGithubProjectUrl(project.repository), + count: Object.values(project.usages).reduce((acc, usage) => acc + usage.length, 0), + }); + }); + + return dependentProjects; +} diff --git a/www/src/utils/getEmptyMessage.tsx b/www/src/utils/getEmptyMessage.tsx new file mode 100644 index 0000000000..22e380787f --- /dev/null +++ b/www/src/utils/getEmptyMessage.tsx @@ -0,0 +1,3 @@ +export default function getEmptyMessage(text: string) { + return `Currently there are no ${text} usage yet`; +} diff --git a/www/src/utils/getGithubProjectUrl.ts b/www/src/utils/getGithubProjectUrl.js similarity index 65% rename from www/src/utils/getGithubProjectUrl.ts rename to www/src/utils/getGithubProjectUrl.js index ddeb75dceb..65fe7cfb52 100644 --- a/www/src/utils/getGithubProjectUrl.ts +++ b/www/src/utils/getGithubProjectUrl.js @@ -1,4 +1,4 @@ -const getGithubProjectUrl = (repository?: string | { type: string, url: string }): string | undefined => { +const getGithubProjectUrl = (repository) => { let repositoryUrl; if (typeof repository === 'string') { repositoryUrl = repository; @@ -9,10 +9,10 @@ const getGithubProjectUrl = (repository?: string | { type: string, url: string } return undefined; } const parts = repositoryUrl.split('/'); - const githubDomainIndex = parts.findIndex((part: string) => part === 'github.com'); + const githubDomainIndex = parts.findIndex((part) => part === 'github.com'); parts.splice(0, githubDomainIndex); const parsedRepositoryUrl = parts.join('/').replace('.git', ''); return `https://${parsedRepositoryUrl}/blob/master`; }; -export default getGithubProjectUrl; +module.exports = getGithubProjectUrl; diff --git a/www/src/utils/getParagonComponentsTypes.js b/www/src/utils/getParagonComponentsTypes.js index 4fc2dccf1f..b3e1e190f0 100644 --- a/www/src/utils/getParagonComponentsTypes.js +++ b/www/src/utils/getParagonComponentsTypes.js @@ -8,6 +8,9 @@ const getParagonComponentsTypes = (components) => { const isContext = !!component.Consumer && !!component.Provider; let componentType; switch (true) { + case componentName.toLowerCase() === 'paragon': + componentType = 'Paragon'; + break; case typeof component === 'string' || typeof component === 'number': componentType = 'Text'; break; @@ -17,7 +20,7 @@ const getParagonComponentsTypes = (components) => { case isFunctionComponent || isObjectComponent || isContext: componentType = 'Component'; break; - case component.constructor.name === 'Object': + case typeof component === 'object': componentType = 'Object'; break; case typeof component === 'function': diff --git a/www/src/utils/removeDotsFromKey.tsx b/www/src/utils/removeDotsFromKey.tsx new file mode 100644 index 0000000000..c1f584f723 --- /dev/null +++ b/www/src/utils/removeDotsFromKey.tsx @@ -0,0 +1,10 @@ +/** + * Removes dots in the keys of the passed object. + * @param {object} object - object with usage insights of Paragon components. + */ +const removeDotsFromKeys = (object) => Object.entries(object).reduce((accumulator, [key, value]) => { + const newKey = key.replace(/\./g, ''); + return { ...accumulator, [newKey]: value }; +}, {}); + +export default removeDotsFromKeys; diff --git a/www/src/utils/usagePropTypes.tsx b/www/src/utils/usagePropTypes.tsx new file mode 100644 index 0000000000..c21cfae7fb --- /dev/null +++ b/www/src/utils/usagePropTypes.tsx @@ -0,0 +1,7 @@ +import PropTypes from 'prop-types'; + +const usagePropTypes = { + data: PropTypes.arrayOf(PropTypes.string).isRequired, +}; + +export default usagePropTypes; diff --git a/www/utils/createCssUtilityClassNodes.js b/www/utils/createCssUtilityClassNodes.js new file mode 100644 index 0000000000..4074fadf40 --- /dev/null +++ b/www/utils/createCssUtilityClassNodes.js @@ -0,0 +1,65 @@ +const sass = require('sass'); +const path = require('path'); +const css = require('css'); + +function createCssUtilityClassNodes({ + actions, + createNodeId, + createContentDigest, +}) { + const { createNode } = actions; + + // We convert to CSS first since we prefer the real values over tokens. + const compiledCSS = sass + .renderSync({ + file: path.resolve(__dirname, '../../scss/core/utilities-only.scss'), + // Resolve tildes the way webpack would in our base npm project + importer(url) { + let resolvedUrl = url; + if (url[0] === '~') { + resolvedUrl = path.resolve(__dirname, '../../node_modules', url.substr(1)); + } + return { file: resolvedUrl }; + }, + }) + .css.toString(); + + const sheet = css.parse(compiledCSS).stylesheet; + + sheet.rules.forEach(({ selectors, position, declarations }) => { + if (!selectors) { return; } + + selectors.forEach(selector => { + if (selector[0] !== '.') { return; } // classes only + + const classSelector = selector.substr(1); + + const nodeData = { + selector: classSelector, + declarations: declarations.map( + ({ property, value }) => `${property}: ${value};`, + ), + isUtility: + declarations.length === 1 + && declarations[0].value.includes('!important'), + }; + + const nodeMeta = { + id: createNodeId( + `rule-${classSelector}-${position.start.line}-${position.end.line}`, + ), + parent: null, + children: [], + internal: { + type: 'CssUtilityClasses', + contentDigest: createContentDigest(nodeData), + }, + }; + + const node = { ...nodeData, ...nodeMeta }; + createNode(node); + }); + }); +} + +module.exports = createCssUtilityClassNodes; diff --git a/www/utils/createPages.js b/www/utils/createPages.js new file mode 100644 index 0000000000..e28cf61e57 --- /dev/null +++ b/www/utils/createPages.js @@ -0,0 +1,93 @@ +const path = require('path'); +const fs = require('fs'); +const { INSIGHTS_PAGES } = require('../src/config'); +const { getThemesSCSSVariables, processComponentSCSSVariables } = require('../theme-utils'); +const componentsUsage = require('../src/utils/componentsUsage'); + +async function createPages(graphql, actions, reporter) { + // Destructure the createPage function from the actions object + const { createPage, createRedirect } = actions; + // MDX transforms markdown generated by gatsby-transformer-react-docgen + // This query filters out all of those markdown nodes and assumes all others + // are for page creation purposes. + const result = await graphql(` + query { + allMdx( + filter: { + parent: { + internal: { owner: { nin: "gatsby-transformer-react-docgen" } } + } + } + ) { + edges { + node { + id + fields { + slug + } + frontmatter { + components + } + slug + } + } + } + } + `); + if (result.errors) { + reporter.panicOnBuild('🚨 ERROR: Loading createPages query'); + } + // Create component detail pages. + const components = result.data.allMdx.edges; + + const themesSCSSVariables = await getThemesSCSSVariables(); + + // you'll call `createPage` for each result + // eslint-disable-next-line no-restricted-syntax + for (const { node } of components) { + const componentDir = node.slug.split('/')[0]; + const variablesPath = path.resolve(__dirname, `../../src/${componentDir}/_variables.scss`); + let scssVariablesData = {}; + + if (fs.existsSync(variablesPath)) { + // eslint-disable-next-line no-await-in-loop + scssVariablesData = await processComponentSCSSVariables(variablesPath, themesSCSSVariables); + } + + createPage({ + // This is the slug you created before + // (or `node.frontmatter.slug`) + path: node.fields.slug, + // This component will wrap our MDX content + component: path.resolve(__dirname, '../src/templates/component-page-template.tsx'), + // You can use the values in this context in + // our page layout component + context: { + id: node.id, + components: node.frontmatter.components || [], + scssVariablesData, + componentsUsageInsights: Object.keys(componentsUsage), + }, + }); + } + + INSIGHTS_PAGES.forEach(({ path: pagePath, tab }) => { + createPage({ + path: pagePath, + component: require.resolve('../src/pages/insights.tsx'), + context: { tab }, + }); + }); + + createRedirect({ + fromPath: '/playroom', + toPath: '/playroom/index.html', + }); + + createRedirect({ + fromPath: '/playroom/preview', + toPath: '/playroom/preview/index.html', + }); +} + +module.exports = createPages; diff --git a/www/utils/onCreateNode.js b/www/utils/onCreateNode.js new file mode 100644 index 0000000000..5950d05518 --- /dev/null +++ b/www/utils/onCreateNode.js @@ -0,0 +1,28 @@ +const { createFilePath } = require('gatsby-source-filesystem'); + +function onCreateNode(node, actions, getNode) { + const { createNodeField } = actions; + // you only want to operate on `Mdx` nodes. If you had content from a + // remote CMS you could also check to see if the parent node was a + // `File` node here + if (node.internal.type === 'Mdx') { + const value = createFilePath({ node, getNode }) + .split('README')[0] + .toLowerCase(); + + const isChangelogNode = node.fileAbsolutePath && node.fileAbsolutePath.endsWith('CHANGELOG.md'); + + createNodeField({ + // Name of the field you are adding + name: 'slug', + // Individual MDX node + node, + // Generated value based on filepath with 'components' prefix. you + // don't need a separating '/' before the value because + // createFilePath returns a path with the leading '/'. + value: isChangelogNode ? 'changelog' : `/components${value}`, + }); + } +} + +module.exports = onCreateNode; diff --git a/www/utils/onCreateWebpackConfig.js b/www/utils/onCreateWebpackConfig.js new file mode 100644 index 0000000000..9b3171fe0e --- /dev/null +++ b/www/utils/onCreateWebpackConfig.js @@ -0,0 +1,15 @@ +const path = require('path'); + +function onCreateWebpackConfig(actions) { + actions.setWebpackConfig({ + resolve: { + alias: { + '~paragon-react': path.resolve(__dirname, '../../src'), + '~paragon-style': path.resolve(__dirname, '../../scss'), + '~paragon-icons': path.resolve(__dirname, '../../icons'), + }, + }, + }); +} + +module.exports = onCreateWebpackConfig;