feat(graph): rework pdv target section & remove unused code (#21159)

This commit is contained in:
MaxKless 2024-01-16 21:12:34 +01:00 committed by GitHub
parent 253c0ff2ab
commit e38b0bb6f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 1914 additions and 1209 deletions

View File

@ -81,6 +81,14 @@ export class ExternalApiImpl extends ExternalApi {
}
}
openProjectDetails(projectName: string, targetName?: string) {
this.router.navigate(
`/project-details/${encodeURIComponent(projectName)}${
targetName ? `?expanded=${encodeURIComponent(targetName)}` : ''
}`
);
}
focusProject(projectName: string) {
this.router.navigate(`/projects/${encodeURIComponent(projectName)}`);
}

View File

@ -1,288 +0,0 @@
import {
ChevronDownIcon,
ChevronRightIcon,
EyeIcon,
PlayIcon,
} from '@heroicons/react/24/outline';
import { getSourceInformation } from './get-source-information';
import useMapState from './use-map-state';
import {
getExternalApiService,
useEnvironmentConfig,
useRouteConstructor,
} from '@nx/graph/shared';
import { useNavigate } from 'react-router-dom';
import { get } from 'http';
import { useEffect } from 'react';
interface JsonLineRendererProps {
jsonData: any;
sourceMap: Record<string, string[]>;
}
export function JsonLineRenderer(props: JsonLineRendererProps) {
let collapsibleSections = new Map<number, number>();
let lines: [string, number][] = [];
let currentLine = 0;
let lineToPropertyPathMap = new Map<number, string>();
let lineToInteractionMap = new Map<
number,
{ target: string; configuration?: string }
>();
const [getCollapsed, setCollapsed] = useMapState<number, boolean>();
const { environment } = useEnvironmentConfig();
const externalApiService = getExternalApiService();
const navigate = useNavigate();
const routeContructor = useRouteConstructor();
function add(value: string, depth: number) {
if (lines.length === currentLine) {
lines.push(['', depth]);
}
lines[currentLine] = [lines[currentLine][0] + value, depth];
}
function processJson(
jsonData: any,
depth = 0,
propertyPath = '',
isLast = false
) {
if (Array.isArray(jsonData)) {
const sectionStart = currentLine;
add('[', depth);
currentLine++;
jsonData.forEach((value, index) => {
const newPropertyPath = `${
propertyPath ? propertyPath + '.' : ''
}${value}`;
lineToPropertyPathMap.set(currentLine, newPropertyPath);
processJson(
value,
depth + 1,
newPropertyPath,
index === jsonData.length - 1
);
});
add(']', depth);
if (!isLast) {
add(',', depth);
}
const sectionEnd = currentLine;
collapsibleSections.set(sectionStart, sectionEnd);
currentLine++;
} else if (jsonData && typeof jsonData === 'object') {
const sectionStart = currentLine;
add('{', depth);
currentLine++;
Object.entries(jsonData).forEach(([key, value], index, array) => {
// skip empty objects
if (
Object.keys(value as any).length === 0 &&
typeof value === 'object'
) {
return;
}
// skip certain root properties
if (
depth === 0 &&
(key === 'sourceRoot' ||
key === 'name' ||
key === '$schema' ||
key === 'tags')
) {
return;
}
add(`"${key}": `, depth);
if (propertyPath === 'targets') {
lineToInteractionMap.set(currentLine, { target: key });
}
if (propertyPath.match(/^targets\..*configurations$/)) {
lineToInteractionMap.set(currentLine, {
target: propertyPath.split('.')[1],
configuration: key,
});
}
const newPropertyPath = `${
propertyPath ? propertyPath + '.' : ''
}${key}`;
lineToPropertyPathMap.set(currentLine, newPropertyPath);
processJson(
value,
depth + 1,
newPropertyPath,
index === array.length - 1
);
});
add('}', depth);
if (!isLast) {
add(',', depth);
}
const sectionEnd = currentLine;
collapsibleSections.set(sectionStart, sectionEnd);
currentLine++;
} else {
add(`"${jsonData}"`, depth);
if (!isLast) {
add(',', depth);
}
currentLine++;
}
}
processJson(props.jsonData);
console.log(lineToInteractionMap);
// start off with all targets & configurations collapsed~
useEffect(() => {
for (const line of lineToInteractionMap.keys()) {
if (!getCollapsed(line)) {
setCollapsed(line, true);
}
}
}, []);
function toggleCollapsed(index: number) {
setCollapsed(index, !getCollapsed(index));
}
function lineIsCollapsed(index: number) {
for (const [start, end] of collapsibleSections) {
if (index > start && index < end) {
if (getCollapsed(start)) {
return true;
}
}
}
return false;
}
function runTarget({
target,
configuration,
}: {
target: string;
configuration?: string;
}) {
const projectName = props.jsonData.name;
externalApiService.postEvent({
type: 'run-task',
payload: { taskId: `${projectName}:${target}` },
});
}
function viewInTaskGraph({
target,
configuration,
}: {
target: string;
configuration?: string;
}) {
const projectName = props.jsonData.name;
if (environment === 'nx-console') {
externalApiService.postEvent({
type: 'open-task-graph',
payload: {
projectName: projectName,
targetName: target,
},
});
} else {
navigate(
routeContructor(
{
pathname: `/tasks/${encodeURIComponent(target)}`,
search: `?projects=${encodeURIComponent(projectName)}`,
},
true
)
);
}
}
return (
<div className="overflow-auto w-full h-full flex">
<div className="h-fit min-h-full w-12 shrink-0 pr-2 border-solid border-r-2 border-slate-700">
{lines.map(([text, indentation], index) => {
if (
lineIsCollapsed(index) ||
index === 0 ||
index === lines.length - 1
) {
return null;
}
const canCollapse =
collapsibleSections.has(index) &&
collapsibleSections.get(index)! - index > 1;
const interaction = lineToInteractionMap.get(index);
return (
<div className="flex justify-end items-center h-6">
{interaction?.target && !interaction?.configuration && (
<EyeIcon
className="h-4 w-4"
onClick={() => viewInTaskGraph(interaction!)}
/>
)}
{environment === 'nx-console' && interaction?.target && (
<PlayIcon
className="h-4 w-4"
onClick={() => runTarget(interaction!)}
/>
)}
{canCollapse && (
<div onClick={() => toggleCollapsed(index)} className="h-4 w-4">
{getCollapsed(index) ? (
<ChevronRightIcon />
) : (
<ChevronDownIcon />
)}
</div>
)}
</div>
);
})}
</div>
<div className="pl-2">
{lines.map(([text, indentation], index) => {
if (
lineIsCollapsed(index) ||
index === 0 ||
index === lines.length - 1
) {
return null;
}
const propertyPathAtLine = lineToPropertyPathMap.get(index);
const sourceInformation = propertyPathAtLine
? getSourceInformation(props.sourceMap, propertyPathAtLine)
: '';
return (
<pre
style={{ paddingLeft: `${indentation}rem` }}
className="group truncate hover:bg-slate-800 h-6"
>
{text}
{getCollapsed(index) ? '...' : ''}
<span className="ml-16 hidden group-hover:inline-block text-sm text-slate-500">
{sourceInformation}
</span>
</pre>
);
})}
</div>
</div>
);
}

View File

@ -1,18 +1,17 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { useNavigate, useRouteLoaderData } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { ProjectGraphProjectNode } from '@nx/devkit';
import { EyeIcon } from '@heroicons/react/24/outline';
import {
getExternalApiService,
useEnvironmentConfig,
useRouteConstructor,
} from '@nx/graph/shared';
import { JsonLineRenderer } from './json-line-renderer';
import { EyeIcon } from '@heroicons/react/24/outline';
import PropertyRenderer from './property-renderer';
import Target from './target';
@ -28,7 +27,7 @@ export function ProjectDetails({
},
sourceMap,
}: ProjectDetailsProps) {
const { environment } = useEnvironmentConfig();
const environment = useEnvironmentConfig()?.environment;
const externalApiService = getExternalApiService();
const navigate = useNavigate();
const routeContructor = useRouteConstructor();
@ -46,35 +45,6 @@ export function ProjectDetails({
}
};
// const projectDataSorted = sortObjectWithTargetsFirst(projectData);
// return (
// <div className="flex flex-col w-full h-full">
// <div className="flex">
// <div className="w-12 pr-2 border-r-2 border-solid border-slate-700">
// <EyeIcon
// className="h-6 w-6 ml-3 mt-3"
// onClick={viewInProjectGraph}
// ></EyeIcon>
// </div>
// <div className="pl-6 pb-6">
// <h1 className="text-4xl flex items-center">
// <span>{name}</span>
// </h1>
// <div className="flex gap-2">
// <span className="text-slate-500 text-xl"> {root}</span>
// {projectData.tags?.map((tag) => (
// <div className="dark:bg-sky-500 text-white rounded px-1">
// {tag}
// </div>
// ))}
// </div>
// </div>
// </div>
// {JsonLineRenderer({ jsonData: projectDataSorted, sourceMap })}
// </div>
// );
return (
<div className="m-4 overflow-auto w-full">
<h1 className="text-2xl flex items-center gap-2">
@ -89,15 +59,17 @@ export function ProjectDetails({
</h2>
<div>
<div className="mb-2">
<h2 className="text-xl">Targets</h2>
<h2 className="text-xl mb-2">Targets</h2>
{Object.entries(projectData.targets ?? {}).map(
([targetName, target]) =>
Target({
([targetName, target]) => {
const props = {
projectName: name,
targetName: targetName,
targetConfiguration: target,
sourceMap,
})
};
return <Target {...props} />;
}
)}
</div>
{Object.entries(projectData).map(([key, value]) => {
@ -123,22 +95,4 @@ export function ProjectDetails({
);
}
// function sortObjectWithTargetsFirst(obj: any) {
// let sortedObj: any = {};
// // If 'targets' exists, set it as the first property
// if (obj.hasOwnProperty('targets')) {
// sortedObj.targets = obj.targets;
// }
// // Copy the rest of the properties
// for (let key in obj) {
// if (key !== 'targets') {
// sortedObj[key] = obj[key];
// }
// }
// return sortedObj;
// }
export default ProjectDetails;

View File

@ -54,7 +54,7 @@ type PropertValueRendererProps = PropertyRendererProps & {
function PropertyValueRenderer(props: PropertValueRendererProps) {
const { propertyKey, propertyValue, sourceMap, keyPrefix, nested } = props;
if (Array.isArray(propertyValue) && propertyValue.length) {
if (propertyValue && Array.isArray(propertyValue) && propertyValue.length) {
return (
<div className="ml-3">
{nested && renderOpening(propertyValue)}
@ -107,7 +107,7 @@ function PropertyValueRenderer(props: PropertValueRendererProps) {
}
function renderOpening(value: any): string {
return Array.isArray(value) && value.length
return value && Array.isArray(value) && value.length
? '['
: value && typeof value === 'object'
? '{'
@ -115,7 +115,7 @@ function renderOpening(value: any): string {
}
function renderClosing(value: any): string {
return Array.isArray(value) && value.length
return value && Array.isArray(value) && value.length
? '],'
: value && typeof value === 'object'
? '},'

View File

@ -1,8 +1,9 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import {
ChevronDownIcon,
ChevronUpIcon,
EyeIcon,
PencilSquareIcon,
PlayIcon,
} from '@heroicons/react/24/outline';
@ -13,8 +14,10 @@ import {
useEnvironmentConfig,
useRouteConstructor,
} from '@nx/graph/shared';
import { useNavigate } from 'react-router-dom';
import PropertyRenderer from './property-renderer';
import { Fence } from '@nx/shared-ui-fence';
import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { FadingCollapsible } from './ui/fading-collapsible.component';
/* eslint-disable-next-line */
export interface TargetProps {
@ -24,16 +27,61 @@ export interface TargetProps {
sourceMap: Record<string, string[]>;
}
export function Target(props: TargetProps) {
const { environment } = useEnvironmentConfig();
export function Target({
projectName,
targetName,
targetConfiguration,
sourceMap,
}: TargetProps) {
const environment = useEnvironmentConfig()?.environment;
const externalApiService = getExternalApiService();
const navigate = useNavigate();
const routeContructor = useRouteConstructor();
const [searchParams, setSearchParams] = useSearchParams();
const [collapsed, setCollapsed] = useState(true);
useEffect(() => {
const expandedSections = searchParams.get('expanded')?.split(',') || [];
setCollapsed(!expandedSections.includes(targetName));
}, [searchParams, targetName]);
function toggleCollapsed() {
setCollapsed((prevState) => {
const newState = !prevState;
setSearchParams((currentSearchParams) => {
const expandedSections =
currentSearchParams.get('expanded')?.split(',') || [];
if (newState) {
const newExpandedSections = expandedSections.filter(
(section) => section !== targetName
);
updateSearchParams(currentSearchParams, newExpandedSections);
} else {
if (!expandedSections.includes(targetName)) {
expandedSections.push(targetName);
updateSearchParams(currentSearchParams, expandedSections);
}
}
return currentSearchParams;
});
return newState;
});
}
function updateSearchParams(params: URLSearchParams, sections: string[]) {
if (sections.length === 0) {
params.delete('expanded');
} else {
params.set('expanded', sections.join(','));
}
}
const runTarget = () => {
externalApiService.postEvent({
type: 'run-task',
payload: { taskId: `${props.projectName}:${props.targetName}` },
payload: { taskId: `${projectName}:${targetName}` },
});
};
@ -42,16 +90,16 @@ export function Target(props: TargetProps) {
externalApiService.postEvent({
type: 'open-task-graph',
payload: {
projectName: props.projectName,
targetName: props.targetName,
projectName: projectName,
targetName: targetName,
},
});
} else {
navigate(
routeContructor(
{
pathname: `/tasks/${encodeURIComponent(props.targetName)}`,
search: `?projects=${encodeURIComponent(props.projectName)}`,
pathname: `/tasks/${encodeURIComponent(targetName)}`,
search: `?projects=${encodeURIComponent(projectName)}`,
},
true
)
@ -59,74 +107,164 @@ export function Target(props: TargetProps) {
}
};
const overrideTarget = () => {
externalApiService.postEvent({
type: 'override-target',
payload: {
projectName: props.projectName,
targetName: props.targetName,
targetConfigString: JSON.stringify(props.targetConfiguration),
},
});
};
const shouldRenderOptions =
targetConfiguration.options &&
(typeof targetConfiguration.options === 'object'
? Object.keys(targetConfiguration.options).length
: true);
const shouldRenderConfigurations =
targetConfiguration.configurations &&
(typeof targetConfiguration.configurations === 'object'
? Object.keys(targetConfiguration.configurations).length
: true);
const shouldDisplayOverrideTarget = () => {
return (
environment === 'nx-console' &&
Object.entries(props.sourceMap ?? {})
.filter(([key]) => key.startsWith(`targets.${props.targetName}`))
.every(([, value]) => value[1] !== 'nx-core-build-project-json-nodes')
);
};
const targetConfigurationSortedAndFiltered = Object.entries(
props.targetConfiguration
)
.filter(([, value]) => {
return (
value &&
(Array.isArray(value) ? value.length : true) &&
(typeof value === 'object' ? Object.keys(value).length : true)
);
})
.sort(([a], [b]) => {
const order = ['executor', 'inputs', 'outputs'];
const indexA = order.indexOf(a);
const indexB = order.indexOf(b);
if (indexA !== -1 && indexB !== -1) {
return indexA - indexB;
} else if (indexA !== -1) {
return -1;
} else if (indexB !== -1) {
return 1;
} else {
return a.localeCompare(b);
<div className="ml-3 mb-3 rounded-md border border-slate-500 relative overflow-hidden">
{/* header */}
<div className="group hover:bg-slate-800 px-2 cursor-pointer ">
<h3
className="text-lg font-bold flex items-center gap-2"
onClick={toggleCollapsed}
>
{targetName}{' '}
<h4 className="text-sm text-slate-600">
{targetConfiguration?.command ??
targetConfiguration.options?.command ??
targetConfiguration.executor}
</h4>
<span
className={
collapsed ? 'hidden group-hover:inline-flex' : 'inline-flex'
}
});
return (
<div className="ml-3 mb-3">
<h3 className="text-lg font-bold flex items-center gap-2">
{props.targetName}{' '}
>
<span
className={`inline-flex justify-center rounded-md p-1 hover:bg-slate-100 hover:dark:bg-slate-700
}`}
>
<EyeIcon
className="h-4 w-4"
onClick={(e) => {
e.stopPropagation();
viewInTaskGraph();
}}
></EyeIcon>
</span>
{environment === 'nx-console' && (
<PlayIcon className="h-5 w-5" onClick={runTarget} />
<span
className={`inline-flex justify-center rounded-md p-1 hover:bg-slate-100 hover:dark:bg-slate-700
}`}
>
<PlayIcon
className="h-4 w-4"
onClick={(e) => {
e.stopPropagation();
runTarget();
}}
/>
</span>
)}
<EyeIcon className="h-5 w-5" onClick={viewInTaskGraph}></EyeIcon>
{shouldDisplayOverrideTarget() && (
<PencilSquareIcon className="h-5 w-5" onClick={overrideTarget} />
</span>
{targetConfiguration.cache && (
<span className="rounded-full inline-block text-xs bg-sky-500 px-2 text-slate-50 ml-auto mr-6">
Cacheable
</span>
)}
</h3>
<div className="ml-3">
{targetConfigurationSortedAndFiltered.map(([key, value]) =>
PropertyRenderer({
propertyKey: key,
propertyValue: value,
keyPrefix: `targets.${props.targetName}`,
sourceMap: props.sourceMap,
})
<div className="absolute top-2 right-3" onClick={toggleCollapsed}>
{collapsed ? (
<ChevronUpIcon className="h-3 w-3" />
) : (
<ChevronDownIcon className="h-3 w-3" />
)}
</div>
</div>
{/* body */}
{!collapsed && (
<div className="pl-5 text-base pb-6 pt-2 ">
{targetConfiguration.inputs && (
<>
<h4 className="font-bold">Inputs</h4>
<ul className="list-disc pl-5">
{targetConfiguration.inputs.map((input) => (
<li> {input.toString()} </li>
))}
</ul>
</>
)}
{targetConfiguration.outputs && (
<>
<h4 className="font-bold pt-2">Outputs</h4>
<ul className="list-disc pl-5">
{targetConfiguration.outputs?.map((output) => (
<li> {output.toString()} </li>
)) ?? <span>no outputs</span>}
</ul>
</>
)}
{targetConfiguration.dependsOn && (
<>
<h4 className="font-bold py-2">Depends On</h4>
<ul className="list-disc pl-5">
{targetConfiguration.dependsOn.map((dep) => (
<li> {dep.toString()} </li>
))}
</ul>
</>
)}
{shouldRenderOptions ? (
<>
<h4 className="font-bold py-2">Options</h4>
<FadingCollapsible>
<Fence
language="json"
command=""
path=""
fileName=""
highlightLines={[]}
lineGroups={{}}
enableCopy={true}
>
{JSON.stringify(targetConfiguration.options, null, 2)}
</Fence>
</FadingCollapsible>
</>
) : (
''
)}
{shouldRenderConfigurations ? (
<>
<h4 className="font-bold py-2">
Configurations{' '}
{targetConfiguration.defaultConfiguration && (
<span
className="ml-3 rounded-full inline-block text-xs bg-sky-500 px-2 text-slate-50 mr-6"
title="Default Configuration"
>
{targetConfiguration.defaultConfiguration}
</span>
)}
</h4>
<FadingCollapsible>
<Fence
language="json"
command=""
path=""
fileName=""
highlightLines={[]}
lineGroups={{}}
enableCopy={true}
>
{JSON.stringify(targetConfiguration.configurations, null, 2)}
</Fence>
</FadingCollapsible>
</>
) : (
''
)}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,55 @@
import { ArrowDownIcon, ArrowUpIcon } from '@heroicons/react/24/outline';
import { ReactNode, useEffect, useRef, useState } from 'react';
export function FadingCollapsible({ children }: { children: ReactNode }) {
const [collapsed, setCollapsed] = useState(true);
const [isCollapsible, setIsCollapsible] = useState(true);
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (contentRef.current) {
setIsCollapsible(contentRef.current.offsetHeight > 300);
}
}, [contentRef, children]);
function toggleCollapsed() {
setCollapsed(!collapsed);
}
const fadeStyles =
collapsed && isCollapsible
? {
maxHeight: '150px',
maskImage: 'linear-gradient(to bottom, black 40%, transparent)',
WebkitMaskImage: 'linear-gradient(to bottom, black 40%, transparent)',
}
: {};
return (
<div
className={`relative overflow-hidden ${
collapsed && isCollapsible ? 'cursor-pointer' : 'max-h-full'
}`}
onClick={() => collapsed && isCollapsible && toggleCollapsed()}
>
<div
className={`${
collapsed && isCollapsible ? 'hover:bg-slate-700' : ''
} rounded-md`}
style={fadeStyles}
>
<div ref={contentRef}>{children}</div>
</div>
{isCollapsible && (
<div
className="h-4 w-4 absolute bottom-2 right-1/2 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
toggleCollapsed();
}}
>
{collapsed ? <ArrowDownIcon /> : <ArrowUpIcon />}
</div>
)}
</div>
);
}

View File

@ -1,21 +0,0 @@
import { useState, useCallback } from 'react';
function useMapState<K, V>(initialMap: Map<K, V> = new Map()) {
const [map, setMap] = useState(new Map(initialMap));
// Function to set a key-value pair in the map
const setKey = useCallback((key: K, value: V) => {
setMap((prevMap) => {
const newMap = new Map(prevMap);
newMap.set(key, value);
return newMap;
});
}, []);
// Function to get a value by key from the map
const getKey = useCallback((key: K) => map.get(key), [map]);
return [getKey, setKey] as const;
}
export default useMapState;

View File

@ -6,7 +6,8 @@
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
]
],
"lib": ["DOM"]
},
"exclude": [
"jest.config.ts",

View File

@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}

View File

@ -0,0 +1,18 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -0,0 +1,7 @@
# shared-ui-fence
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test shared-ui-fence` to execute the unit tests via [Jest](https://jestjs.io).

View File

@ -0,0 +1,10 @@
/* eslint-disable */
export default {
displayName: 'shared-ui-fence',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/nx-dev/shared-ui-fence',
};

View File

@ -0,0 +1,20 @@
{
"name": "shared-ui-fence",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "nx-dev/shared-ui-fence/src",
"projectType": "library",
"tags": [],
"targets": {
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "nx-dev/shared-ui-fence/jest.config.ts",
"passWithNoTests": true
}
}
}
}

View File

@ -0,0 +1,3 @@
export * from './lib/fence.component';
export { TerminalOutput } from './lib/fences/terminal-output.component';
export { TerminalShellWrapper } from './lib/fences/terminal-shell.component';

View File

@ -11,8 +11,7 @@ import { CopyToClipboard } from 'react-copy-to-clipboard';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { CodeOutput } from './fences/code-output.component';
import { TerminalOutput } from './fences/terminal-output.component';
import { useRouter } from 'next/router';
import { Selector } from '@nx/nx-dev/ui-common';
import { Selector } from '@nx/shared-ui-selector';
function resolveLanguage(lang: string) {
switch (lang) {
@ -57,32 +56,6 @@ function CodeWrapper(options: {
);
}
const useUrlHash = (initialValue: string) => {
const router = useRouter();
const [hash, setHash] = useState(initialValue);
const updateHash = (str: string) => {
if (!str) return;
setHash(str.split('#')[1]);
};
useEffect(() => {
const onWindowHashChange = () => updateHash(window.location.hash);
const onNextJSHashChange = (url: string) => updateHash(url);
router.events.on('hashChangeStart', onNextJSHashChange);
window.addEventListener('hashchange', onWindowHashChange);
window.addEventListener('load', onWindowHashChange);
return () => {
router.events.off('hashChangeStart', onNextJSHashChange);
window.removeEventListener('load', onWindowHashChange);
window.removeEventListener('hashchange', onWindowHashChange);
};
}, [router.asPath, router.events]);
return hash;
};
// pre-process the highlightLines to expand ranges like
// ["8-10", 19, 22] => [8,9,10,19,22]
function processHighlightLines(highlightLines: any): number[] {
@ -109,6 +82,19 @@ function processHighlightLines(highlightLines: any): number[] {
);
}
export interface FenceProps {
children: string;
command: string;
path: string;
fileName: string;
highlightLines: number[];
lineGroups: Record<string, number[]>;
language: string;
enableCopy: boolean;
selectedLineGroup?: string;
onLineGroupSelectionChange?: (selection: string) => void;
}
export function Fence({
children,
command,
@ -118,19 +104,9 @@ export function Fence({
highlightLines,
language,
enableCopy,
}: {
children: string;
command: string;
path: string;
fileName: string;
highlightLines: number[];
lineGroups: Record<string, number[]>;
language: string;
enableCopy: boolean;
}) {
const { push, asPath } = useRouter();
const hash = decodeURIComponent(useUrlHash(''));
selectedLineGroup,
onLineGroupSelectionChange,
}: FenceProps) {
if (highlightLines) {
highlightLines = processHighlightLines(highlightLines);
}
@ -138,7 +114,9 @@ export function Fence({
function lineNumberStyle(lineNumber: number) {
if (
(highlightLines && highlightLines.includes(lineNumber)) ||
(lineGroups[hash] && lineGroups[hash].includes(lineNumber))
(selectedLineGroup &&
lineGroups[selectedLineGroup] &&
lineGroups[selectedLineGroup].includes(lineNumber))
) {
return {
fontSize: 0,
@ -168,7 +146,7 @@ export function Fence({
});
}
let selectedOption =
highlightOptions.find((option) => option.value === hash) ||
highlightOptions.find((option) => option.value === selectedLineGroup) ||
highlightOptions[0];
const [copied, setCopied] = useState(false);
useEffect(() => {
@ -185,10 +163,9 @@ export function Fence({
const showRescopeMessage =
children.includes('@nx/') || command.includes('@nx/');
function highlightChange(item: { label: string; value: string }) {
push(asPath.split('#')[0] + '#' + item.value);
onLineGroupSelectionChange?.(item.value);
}
return (
<div className="my-8 w-full">
<div className="code-block group relative w-full">
<div>
<div className="absolute top-0 right-0 z-10 flex">
@ -257,6 +234,5 @@ export function Fence({
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}

View File

@ -0,0 +1,24 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
],
"lib": ["DOM", "es2019"]
},
"exclude": [
"jest.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.spec.tsx",
"src/**/*.test.tsx",
"src/**/*.spec.js",
"src/**/*.test.js",
"src/**/*.spec.jsx",
"src/**/*.test.jsx"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}

View File

@ -0,0 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}

View File

@ -0,0 +1,18 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -0,0 +1,7 @@
# shared-ui-selector
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test shared-ui-selector` to execute the unit tests via [Jest](https://jestjs.io).

View File

@ -0,0 +1,10 @@
/* eslint-disable */
export default {
displayName: 'shared-ui-selector',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/nx-dev/shared-ui-selector',
};

View File

@ -0,0 +1,20 @@
{
"name": "shared-ui-selector",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "nx-dev/shared-ui-selector/src",
"projectType": "library",
"tags": [],
"targets": {
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "nx-dev/shared-ui-selector/jest.config.ts",
"passWithNoTests": true
}
}
}
}

View File

@ -0,0 +1 @@
export * from './lib/selector';

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}

View File

@ -0,0 +1,24 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
]
},
"exclude": [
"jest.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.spec.tsx",
"src/**/*.test.tsx",
"src/**/*.spec.js",
"src/**/*.test.js",
"src/**/*.spec.jsx",
"src/**/*.test.jsx"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}

View File

@ -0,0 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}

View File

@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { CopyToClipboard } from 'react-copy-to-clipboard';
// @ts-ignore
import SyntaxHighlighter from 'react-syntax-highlighter';
/* eslint-disable-next-line */

View File

@ -7,7 +7,6 @@ export * from './lib/champion-perks';
export * from './lib/header';
export * from './lib/flip-card';
export * from './lib/footer';
export * from './lib/selector';
export * from './lib/sidebar-container';
export * from './lib/sidebar';
export * from './lib/nx-users-showcase';

View File

@ -8,8 +8,6 @@ import {
} from '@markdoc/markdoc';
import { load as yamlLoad } from 'js-yaml';
import React, { ReactNode } from 'react';
import { Fence } from './lib/nodes/fence.component';
import { fence } from './lib/nodes/fence.schema';
import { Heading } from './lib/nodes/heading.component';
import { heading } from './lib/nodes/heading.schema';
import { getImageSchema } from './lib/nodes/image.schema';
@ -55,6 +53,8 @@ import { VideoLink, videoLink } from './lib/tags/video-link.component';
import { Pill } from './lib/tags/pill.component';
import { pill } from './lib/tags/pill.schema';
import { frameworkIcons } from './lib/icons';
import { fence } from './lib/nodes/fence.schema';
import { FenceWrapper } from './lib/nodes/fence-wrapper.component';
// TODO fix this export
export { GithubRepository } from './lib/tags/github-repository.component';
@ -103,7 +103,7 @@ export const getMarkdocCustomConfig = (
Disclosure,
LinkCard,
CustomLink,
Fence,
FenceWrapper,
GithubRepository,
StackblitzButton,
Graph,

View File

@ -0,0 +1,47 @@
import { Fence, FenceProps } from '@nx/shared-ui-fence';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
const useUrlHash = (initialValue: string) => {
const router = useRouter();
const [hash, setHash] = useState(initialValue);
const updateHash = (str: string) => {
if (!str) return;
setHash(str.split('#')[1]);
};
useEffect(() => {
const onWindowHashChange = () => updateHash(window.location.hash);
const onNextJSHashChange = (url: string) => updateHash(url);
router.events.on('hashChangeStart', onNextJSHashChange);
window.addEventListener('hashchange', onWindowHashChange);
window.addEventListener('load', onWindowHashChange);
return () => {
router.events.off('hashChangeStart', onNextJSHashChange);
window.removeEventListener('load', onWindowHashChange);
window.removeEventListener('hashchange', onWindowHashChange);
};
}, [router.asPath, router.events]);
return hash;
};
export function FenceWrapper(props: FenceProps) {
const { push, asPath } = useRouter();
const hash = decodeURIComponent(useUrlHash(''));
const modifiedProps: FenceProps = {
...props,
selectedLineGroup: hash,
onLineGroupSelectionChange: (selection: string) => {
push(asPath.split('#')[0] + '#' + selection);
},
};
return (
<div className="my-8 w-full">
<Fence {...modifiedProps} />
</div>
);
}

View File

@ -1,7 +1,7 @@
import { Schema, Tag } from '@markdoc/markdoc';
export const fence: Schema = {
render: 'Fence',
render: 'FenceWrapper',
attributes: {
content: { type: 'String', render: false, required: true },
language: { type: 'String' },
@ -18,6 +18,6 @@ export const fence: Schema = {
const children = node.children.length
? node.transformChildren(config)
: [node.attributes['content']];
return new Tag('Fence', attributes, children);
return new Tag('FenceWrapper', attributes, children);
},
};

View File

@ -1,6 +1,6 @@
import { TerminalShellWrapper } from '@nx/shared-ui-fence';
import { VideoLoop } from './video-loop.component';
import { Schema } from '@markdoc/markdoc';
import { TerminalShellWrapper } from '../nodes/fences/terminal-shell.component';
export const terminalVideo: Schema = {
render: 'TerminalVideo',

View File

@ -106,7 +106,7 @@
"@swc/cli": "0.1.62",
"@swc/core": "^1.3.85",
"@swc/jest": "^0.2.20",
"@testing-library/react": "13.4.0",
"@testing-library/react": "14.0.0",
"@types/cytoscape": "^3.18.2",
"@types/detect-port": "^1.3.2",
"@types/ejs": "3.1.2",
@ -123,8 +123,8 @@
"@types/node": "18.16.9",
"@types/npm-package-arg": "6.1.1",
"@types/prettier": "^2.6.2",
"@types/react": "18.2.24",
"@types/react-dom": "18.2.9",
"@types/react": "18.2.33",
"@types/react-dom": "18.2.14",
"@types/semver": "^7.5.2",
"@types/tar-stream": "^2.2.2",
"@types/tmp": "^0.2.0",
@ -169,10 +169,10 @@
"eslint-config-next": "13.1.1",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-cypress": "2.14.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-playwright": "^0.15.3",
"eslint-plugin-react": "7.31.11",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-storybook": "^0.6.12",
"express": "^4.18.1",
@ -249,7 +249,7 @@
"react-markdown": "^8.0.7",
"react-redux": "8.0.5",
"react-refresh": "^0.10.0",
"react-router-dom": "^6.11.2",
"react-router-dom": "^6.21.2",
"react-textarea-autosize": "^8.5.3",
"regenerator-runtime": "0.13.7",
"resolve.exports": "1.1.0",
@ -371,4 +371,3 @@
]
}
}

View File

@ -9,7 +9,12 @@ describe('createWatchPaths', () => {
const testDir = joinPathFragments(workspaceRoot, 'e2e/remix');
const paths = await createWatchPaths(testDir);
expect(paths).toEqual(['../../packages', '../../graph', '../../e2e/utils']);
expect(paths).toEqual([
'../../packages',
'../../graph',
'../../nx-dev',
'../../e2e/utils',
]);
});
});

1849
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -102,6 +102,8 @@
"@nx/remix/*": ["packages/remix/*"],
"@nx/rollup": ["packages/rollup"],
"@nx/rollup/*": ["packages/rollup/*"],
"@nx/shared-ui-fence": ["nx-dev/shared-ui-fence/src/index.ts"],
"@nx/shared-ui-selector": ["nx-dev/shared-ui-selector/src/index.ts"],
"@nx/storybook": ["packages/storybook"],
"@nx/storybook/*": ["packages/storybook/*"],
"@nx/typedoc-theme": ["typedoc-theme/src/index.ts"],