Skip to content

Commit

Permalink
feat(graph): add target groups
Browse files Browse the repository at this point in the history
  • Loading branch information
xiongemi committed Apr 16, 2024
1 parent 6b60d21 commit cc0d56c
Show file tree
Hide file tree
Showing 68 changed files with 2,957 additions and 714 deletions.
2 changes: 1 addition & 1 deletion graph/client/jest.config.ts
Expand Up @@ -10,7 +10,7 @@ export default {
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/nx-dev/nx-dev',
coverageDirectory: '../../coverage/graph/client',
// The mock for widnow.matchMedia has to be in a separete file and imported before the components to test
// for more info check : // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
modulePathIgnorePatterns: [
Expand Down
8 changes: 7 additions & 1 deletion graph/client/src/app/app.tsx
@@ -1,4 +1,6 @@
import { themeInit } from '@nx/graph/ui-theme';
import { rootStore } from '@nx/graph/state';
import { Provider as StoreProvider } from 'react-redux';
import { rankDirInit } from './rankdir-resolver';
import { RouterProvider } from 'react-router-dom';
import { getRouter } from './get-router';
Expand All @@ -7,5 +9,9 @@ themeInit();
rankDirInit();

export function App() {
return <RouterProvider router={getRouter()} />;
return (
<StoreProvider store={rootStore}>
<RouterProvider router={getRouter()} />
</StoreProvider>
);
}
2 changes: 1 addition & 1 deletion graph/client/src/app/external-api-impl.ts
Expand Up @@ -94,7 +94,7 @@ export class ExternalApiImpl extends ExternalApi {
const currentLocation = this.router.state.location;

const searchParams = new URLSearchParams(currentLocation.search);
searchParams.set('expanded', targetName);
searchParams.set('targetName', targetName);

const newUrl = `${currentLocation.pathname}?${searchParams.toString()}`;
this.router.navigate(newUrl);
Expand Down
4 changes: 2 additions & 2 deletions graph/client/src/app/ui-components/error-boundary.tsx
Expand Up @@ -3,7 +3,7 @@ import { ProjectDetailsHeader } from 'graph/project-details/src/lib/project-deta
import { useRouteError } from 'react-router-dom';

export function ErrorBoundary() {
let error = useRouteError()?.toString();
let error = useRouteError();
console.error(error);
const environment = useEnvironmentConfig()?.environment;

Expand All @@ -20,7 +20,7 @@ export function ErrorBoundary() {
<h1 className="text-4xl mb-4 dark:text-slate-100">Error</h1>
<div>
<p className="text-lg mb-4 dark:text-slate-200">{message}</p>
<p className="text-sm">Error message: {error}</p>
<p className="text-sm">Error message: {error?.toString()}</p>
</div>
</div>
);
Expand Down
10 changes: 10 additions & 0 deletions graph/client/src/styles.css
Expand Up @@ -44,3 +44,13 @@
opacity: 1;
visibility: visible;
}

/* Dark mode */
html.dark .adaptive-icon {
/* fill: white; */
filter: invert(1);
}

.adaptive-icon {
fill: black;
}
4 changes: 2 additions & 2 deletions graph/project-details/src/lib/project-details-page.tsx
@@ -1,12 +1,12 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { ProjectGraphProjectNode } from '@nx/devkit';
import type { ProjectGraphProjectNode } from '@nx/devkit';
import {
ScrollRestoration,
useParams,
useRouteLoaderData,
} from 'react-router-dom';
import { ProjectDetailsWrapper } from './project-details-wrapper';
import ProjectDetailsWrapper from './project-details-wrapper';
import {
fetchProjectGraph,
getProjectGraphDataService,
Expand Down
42 changes: 42 additions & 0 deletions graph/project-details/src/lib/project-details-wrapper.state.ts
@@ -0,0 +1,42 @@
import {
AppDispatch,
RootState,
expandTargetActions,
getExpandedTargets,
getSelectedTargetGroup,
selectTargetGroupActions,
} from '@nx/graph/state';

const mapStateToProps = (state: RootState) => {
return {
expandTargets: getExpandedTargets(state),
selectedTargetGroup: getSelectedTargetGroup(state),
};
};

const mapDispatchToProps = (dispatch: AppDispatch) => {
return {
setExpandTargets(targets: string[]) {
dispatch(expandTargetActions.setExpandTargets(targets));
},
selectTargetGroup(targetGroup: string) {
dispatch(selectTargetGroupActions.selectTargetGroup(targetGroup));
},
collapseAllTargets() {
dispatch(expandTargetActions.collapseAllTargets());
},
clearTargetGroup() {
dispatch(selectTargetGroupActions.clearTargetGroup());
},
};
};

type mapStateToPropsType = ReturnType<typeof mapStateToProps>;
type mapDispatchToPropsType = ReturnType<typeof mapDispatchToProps>;

export {
mapStateToProps,
mapDispatchToProps,
mapStateToPropsType,
mapDispatchToPropsType,
};
176 changes: 110 additions & 66 deletions graph/project-details/src/lib/project-details-wrapper.tsx
@@ -1,28 +1,42 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars

import { useNavigate, useSearchParams } from 'react-router-dom';

/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { ProjectGraphProjectNode } from '@nx/devkit';
import type { ProjectGraphProjectNode } from '@nx/devkit';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { connect } from 'react-redux';
import {
getExternalApiService,
useEnvironmentConfig,
useRouteConstructor,
} from '@nx/graph/shared';
import {
ProjectDetails,
ProjectDetailsImperativeHandle,
defaultSelectTargetGroup,
getTargetGroupForTarget,
} from '@nx/graph/ui-project-details';
import { useCallback, useLayoutEffect, useRef } from 'react';
import { useCallback, useEffect } from 'react';
import {
mapStateToProps,
mapDispatchToProps,
mapStateToPropsType,
mapDispatchToPropsType,
} from './project-details-wrapper.state';

export interface ProjectDetailsProps {
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
}
type ProjectDetailsProps = mapStateToPropsType &
mapDispatchToPropsType & {
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
};

export function ProjectDetailsWrapper(props: ProjectDetailsProps) {
const projectDetailsRef = useRef<ProjectDetailsImperativeHandle>(null);
export function ProjectDetailsWrapperComponent({
project,
sourceMap,
setExpandTargets,
expandTargets,
selectedTargetGroup,
selectTargetGroup,
clearTargetGroup,
collapseAllTargets,
}: ProjectDetailsProps) {
const environment = useEnvironmentConfig()?.environment;
const externalApiService = getExternalApiService();
const navigate = useNavigate();
Expand Down Expand Up @@ -85,75 +99,105 @@ export function ProjectDetailsWrapper(props: ProjectDetailsProps) {
[externalApiService]
);

const updateSearchParams = (params: URLSearchParams, sections: string[]) => {
if (sections.length === 0) {
params.delete('expanded');
const updateSearchParams = (
params: URLSearchParams,
targetGroup: string | null,
targetNames: string[]
) => {
if (targetGroup) {
params.set('targetGroup', targetGroup);
} else {
params.delete('targetGroup');
}
if (targetNames.length === 0) {
params.delete('targetName');
} else {
params.set('expanded', sections.join(','));
params.set('targetName', targetNames.join(','));
}
};

const handleTargetCollapse = useCallback(
(targetName: string) => {
const expandedSections = searchParams.get('expanded')?.split(',') || [];
if (!expandedSections.includes(targetName)) return;
const newExpandedSections = expandedSections.filter(
(section) => section !== targetName
);
setSearchParams(
(currentSearchParams) => {
updateSearchParams(currentSearchParams, newExpandedSections);
return currentSearchParams;
},
{
replace: true,
preventScrollReset: true,
}
);
},
[setSearchParams, searchParams]
);
useEffect(() => {
if (!project.data.targets) return;

const handleTargetExpand = useCallback(
(targetName: string) => {
const expandedSections = searchParams.get('expanded')?.split(',') || [];
if (expandedSections.includes(targetName)) return;
expandedSections.push(targetName);
setSearchParams(
(currentSearchParams) => {
updateSearchParams(currentSearchParams, expandedSections);
return currentSearchParams;
},
{ replace: true, preventScrollReset: true }
);
},
[setSearchParams, searchParams]
);
const selectedTargetGroupParams = searchParams.get('targetGroup');
if (
selectedTargetGroupParams &&
selectedTargetGroup !== selectedTargetGroupParams
) {
selectTargetGroup(selectedTargetGroupParams);
} else if (!selectedTargetGroupParams) {
selectTargetGroup(defaultSelectTargetGroup(project)); // set first target group as default
}

useLayoutEffect(() => {
if (!props.project.data.targets) return;
const expandedTargetsParams =
searchParams.get('targetName')?.split(',') || [];
if (expandedTargetsParams.length > 0) {
setExpandTargets(expandedTargetsParams);
}

const expandedSections = searchParams.get('expanded')?.split(',') || [];
for (const targetName of Object.keys(props.project.data.targets)) {
if (expandedSections.includes(targetName)) {
projectDetailsRef.current?.expandTarget(targetName);
} else {
projectDetailsRef.current?.collapseTarget(targetName);
}
const targetName = searchParams.get('targetName');
if (targetName) {
const targetGroup = getTargetGroupForTarget(targetName, project);
selectTargetGroup(targetGroup);
setExpandTargets([targetName]);
}

return () => {
clearTargetGroup();
collapseAllTargets();
searchParams.delete('targetGroup');
searchParams.delete('targetName');
setSearchParams(searchParams, { replace: true });
};
}, []); // only run on mount

useEffect(() => {
if (!project.data.targets) return;

const selectedTargetGroupParams = searchParams.get('targetGroup');
const expandedTargetsParams =
searchParams.get('targetName')?.split(',') || [];

if (
selectedTargetGroup === selectedTargetGroupParams &&
expandedTargetsParams.join(',') === expandTargets.join(',')
) {
return;
}
}, [searchParams, props.project.data.targets, projectDetailsRef]);

setSearchParams(
(currentSearchParams) => {
updateSearchParams(
currentSearchParams,
selectedTargetGroup,
expandTargets
);
return currentSearchParams;
},
{ replace: true, preventScrollReset: true }
);
}, [
expandTargets,
selectedTargetGroup,
project.data.targets,
setExpandTargets,
searchParams,
setSearchParams,
]);

return (
<ProjectDetails
ref={projectDetailsRef}
{...props}
onTargetCollapse={handleTargetCollapse}
onTargetExpand={handleTargetExpand}
project={project}
sourceMap={sourceMap}
onViewInProjectGraph={handleViewInProjectGraph}
onViewInTaskGraph={handleViewInTaskGraph}
onRunTarget={environment === 'nx-console' ? handleRunTarget : undefined}
/>
);
}

export const ProjectDetailsWrapper = connect(
mapStateToProps,
mapDispatchToProps
)(ProjectDetailsWrapperComponent);
export default ProjectDetailsWrapper;
12 changes: 12 additions & 0 deletions graph/state/.babelrc
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
18 changes: 18 additions & 0 deletions graph/state/.eslintrc.json
@@ -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": {}
}
]
}
11 changes: 11 additions & 0 deletions graph/state/project.json
@@ -0,0 +1,11 @@
{
"name": "graph-state",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "graph/state/src",
"projectType": "library",
"tags": [],
"// targets": "to see all targets run: nx show project ui-icons --web",
"targets": {
"lint": {}
}
}
6 changes: 6 additions & 0 deletions graph/state/src/index.ts
@@ -0,0 +1,6 @@
export * from './lib/expand-targets/expand-targets.slice';
export * from './lib/select-target-group/select-target-group.slice';
export * from './lib/root/root-state.initial';
export * from './lib/root/root.reducer';
export * from './lib/root/root.store';
export * from './lib/store.decorator';

0 comments on commit cc0d56c

Please sign in to comment.