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;