Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BD-46] docs: refactoring Usage Insights #2267

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 7 additions & 184 deletions www/gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
PKulkoRaccoonGang marked this conversation as resolved.
Show resolved Hide resolved

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 });
Expand Down
58 changes: 58 additions & 0 deletions www/src/components/insights/ComponentUsage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="mb-5">
<h3 className="mb-4">{name}</h3>
<DataTable
isExpandable
isSortable
itemCount={componentUsageInProjects.length} // eslint-disable-line
data={componentUsageInProjects}
renderRowSubComponent={({ row }: IComponentUsageExamples) => (
<ComponentUsageExamples row={row} componentName={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' },
]}
>
<DataTable.Table />
<DataTable.EmptyTable content="No usages" />
</DataTable>
</div>
);
}

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;
32 changes: 32 additions & 0 deletions www/src/components/insights/ComponentsUsage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="pt-5 mb-5">
{data.length ? data.sort().map(name => {
if (filteredComponentsUsage[name]) {
return (
<ComponentUsage
key={name}
name={name}
componentUsageInProjects={filteredComponentsUsage[name]}
/>
);
}
return null;
}) : getEmptyMessage('components')}
</div>
);
}

ComponentsUsage.propTypes = usagePropTypes;

export default ComponentsUsage;
24 changes: 24 additions & 0 deletions www/src/components/insights/HooksUsage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="pt-5 mb-5">
{data.length ? data.sort().map(name => (
<ComponentUsage
key={name}
name={name}
componentUsageInProjects={componentsUsage[name]}
/>
)) : getEmptyMessage('hooks')}
</div>
);
}

HooksUsage.propTypes = usagePropTypes;

export default HooksUsage;
24 changes: 24 additions & 0 deletions www/src/components/insights/IconsUsage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="pt-5 mb-5">
{data.length ? data.sort().map(name => (
<ComponentUsage
key={name}
name={name}
componentUsageInProjects={componentsUsage[name]}
/>
)) : getEmptyMessage('utils')}
</div>
);
}

IconsUsage.propTypes = usagePropTypes;

export default IconsUsage;
42 changes: 42 additions & 0 deletions www/src/components/insights/ProjectsUsage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="pt-5 mb-5">
<h3 className="mb-4">Projects in Open edX consuming Paragon</h3>
<DataTable
isExpandable
isSortable
itemCount={dependentProjects.length}
data={dependentProjects}
renderRowSubComponent={({ row }: IProjectUsageExamples) => <ProjectUsageExamples row={row} />}
columns={[
{
id: 'expander',
Header: DataTable.ExpandAll,
Cell: DataTable.ExpandRow,
},
{
Header: 'Project Name',
accessor: 'folderName',
},
{ Header: 'Paragon Version', accessor: 'version' },
{ Header: 'Import Count', accessor: 'count' },
]}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable content="No projects" />
<DataTable.TableFooter />
</DataTable>
</div>
);
}

export default ProjectsUsage;
Loading