Skip to content

Commit

Permalink
Package Graph (#102)
Browse files Browse the repository at this point in the history
* Package Graph

* Vis-network and new query argument in PackagesHandler GET route

* Added graph to jupyter_conda

* Changes

* Cancel task

* delete jupyter_conda bundles

* Check test

* highlight and font size

* eslint

* Apply suggestions from code review

* Apply some more suggestions from code review

* Fixing usage when mamba is missing

* Fix eslint error

Co-authored-by: Frédéric Collonval <[email protected]>
  • Loading branch information
hbcarlos and fcollonval authored Nov 28, 2020
1 parent 82692ee commit 4b8378f
Show file tree
Hide file tree
Showing 10 changed files with 596 additions and 23 deletions.
29 changes: 29 additions & 0 deletions mamba_gator/envmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,35 @@ async def env_packages(self, env: str) -> Dict[str, List[str]]:

return {"packages": [normalize_pkg_info(package) for package in data]}

async def pkg_depends(self, pkg: str) -> Dict[str, List[str]]:
"""List environment packages dependencies.
Args:
pkg (str): Package name
Returns:
{"package": List[dependencies]}
"""
if self.manager != "mamba" :
self.log.warning("Package manager '{}' does not support dependency query.".format(self.manager))
return { pkg: None }

resp = {}
ans = await self._execute(self.manager, "repoquery", "depends", "--json", pkg)
_, output = ans
query = self._clean_conda_json(output)

if "error" not in query:
for dep in query['result']['pkgs'] :
if type(dep) is dict :
deps = dep.get('depends', None)
if deps :
resp[dep['name']] = deps
else :
resp[dep['name']] = []

return resp

async def list_available(self) -> Dict[str, List[Dict[str, str]]]:
"""List all available packages
Expand Down
12 changes: 9 additions & 3 deletions mamba_gator/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,13 +349,19 @@ async def get(self):
"""`GET /packages` Search for packages.
Query arguments:
dependencies: 0 (default) or 1
query (str): optional string query
"""
dependencies = self.get_query_argument("dependencies", 0)
query = self.get_query_argument("query", "")

idx = None
if query: # Specific search
idx = self._stack.put(self.env_manager.package_search, query)
if query:
if dependencies :
idx = self._stack.put(self.env_manager.pkg_depends, query)

else: # Specific search
idx = self._stack.put(self.env_manager.package_search, query)

else: # List all available
cache_file = os.path.join(tempfile.gettempdir(), AVAILABLE_CACHE + ".json")
Expand Down
5 changes: 5 additions & 0 deletions mamba_gator/rest_api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ paths:
produces:
- "application/json"
parameters:
- name: "dependencies"
in: "query"
description: "Whether to return the package dependencies or doing a generic search"
type: "number"
default: "0"
- name: "query"
in: "query"
description: "Query string to pass to conda search"
Expand Down
3 changes: 3 additions & 0 deletions packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
"@lumino/coreutils": "^1.3.0",
"@lumino/signaling": "^1.2.0",
"@lumino/widgets": "^1.6.0",
"d3": "^5.5.0",
"jupyterlab_toastify": "^4.1.2",
"react-d3-graph": "^2.5.0",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.6",
"semver": "^6.0.0||^7.0.0",
Expand All @@ -62,6 +64,7 @@
"@jupyterlab/testutils": "^2.2.2",
"@types/jest": "^25",
"@types/react": "~16.9.0",
"@types/react-d3-graph": "^2.3.4",
"@types/react-virtualized-auto-sizer": "^1.0.0",
"@types/react-window": "^1.8.2",
"@types/semver": "^7.3.1",
Expand Down
23 changes: 20 additions & 3 deletions packages/common/src/components/CondaPkgList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export interface IPkgListProps {
* Package item version selection handler
*/
onPkgChange: (pkg: Conda.IPackage, version: string) => void;
/**
* Package item graph dependencies handler
*/
onPkgGraph: (pkg: Conda.IPackage) => void;
}

/** React component for the package list */
Expand Down Expand Up @@ -157,6 +161,21 @@ export class CondaPkgList extends React.Component<IPkgListProps> {
return <span>{pkg.name}</span>;
};

protected versionRender = (pkg: Conda.IPackage): JSX.Element => (
<a
className={pkg.updatable ? Style.Updatable : undefined}
href="#"
onClick={(evt): void => {
evt.stopPropagation();
this.props.onPkgGraph(pkg);
}}
rel="noopener noreferrer"
title="Show dependency graph"
>
{pkg.version_installed}
</a>
);

protected rowClassName = (index: number, pkg: Conda.IPackage): string => {
if (index >= 0) {
const isSelected = this.isSelected(pkg);
Expand Down Expand Up @@ -194,9 +213,7 @@ export class CondaPkgList extends React.Component<IPkgListProps> {
</div>
)}
<div className={classes(Style.Cell, Style.VersionSize)} role="gridcell">
<span className={pkg.updatable ? Style.Updatable : undefined}>
{pkg.version_installed}
</span>
{this.versionRender(pkg)}
</div>
<div className={classes(Style.Cell, Style.ChangeSize)} role="gridcell">
{this.changeRender(pkg)}
Expand Down
12 changes: 11 additions & 1 deletion packages/common/src/components/CondaPkgPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { showDialog } from '@jupyterlab/apputils';
import { showDialog, Dialog } from '@jupyterlab/apputils';
import { INotification } from 'jupyterlab_toastify';
import * as React from 'react';
import semver from 'semver';
Expand All @@ -10,6 +10,7 @@ import {
PACKAGE_TOOLBAR_HEIGHT,
PkgFilters
} from './CondaPkgToolBar';
import { PkgGraphWidget } from './PkgGraph';

// Minimal panel width to show package description
const PANEL_SMALL_WIDTH = 500;
Expand Down Expand Up @@ -239,6 +240,14 @@ export class CondaPkgPanel extends React.Component<
});
}

handleDependenciesGraph = (pkg: Conda.IPackage): void => {
showDialog({
title: pkg.name,
body: new PkgGraphWidget(this._model, pkg.name),
buttons: [Dialog.okButton()]
});
};

handleSearch(event: any): void {
if (this.state.isApplyingChanges) {
return;
Expand Down Expand Up @@ -505,6 +514,7 @@ export class CondaPkgPanel extends React.Component<
packages={searchPkgs}
onPkgClick={this.handleClick}
onPkgChange={this.handleVersionSelection}
onPkgGraph={this.handleDependenciesGraph}
/>
</div>
);
Expand Down
174 changes: 174 additions & 0 deletions packages/common/src/components/PkgGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { ReactWidget } from '@jupyterlab/apputils';

import {
Graph,
GraphData,
GraphNode,
GraphLink,
GraphConfiguration
} from 'react-d3-graph';

import * as React from 'react';

import { Conda } from '../tokens';

/**
* Package graph property
*/
export interface IPkgGraphProps {
/**
* Package manager for the selected environment
*/
pkgManager: Conda.IPackageManager;
/**
* Package name
*/
package: string;
/**
* Graph configuration
*/
config: GraphConfiguration<GraphNode, GraphLink> | Record<string, any>;
}

/**
* Package graph state
*/
export interface IPkgGraphState {
/**
* Graph data
*/
data: GraphData<GraphNode, GraphLink> | null;
/**
* Error message
*/
error: React.ReactNode;
}

export class PkgGraph extends React.Component<IPkgGraphProps, IPkgGraphState> {
public static defaultProps: Partial<IPkgGraphProps> = {
config: {
directed: true,
collapsible: true,
highlightDegree: 1,
highlightOpacity: 0.1,
nodeHighlightBehavior: true,
linkHighlightBehavior: true,
node: {
color: 'var(--jp-brand-color1)',
highlightColor: 'var(--jp-brand-color2)',
highlightStrokeColor: 'var(--jp-brand-color2)',
highlightFontSize: 'var(--jp-ui-font-size0)',
fontSize: '--jp-ui-font-size0',
fontColor: 'var(--jp-ui-font-color1)'
},
link: {
highlightColor: 'var(--jp-brand-color2)'
}
}
};

constructor(props: IPkgGraphProps) {
super(props);
this.state = {
data: null,
error: null
};
}

componentDidMount(): void {
this._updatePackages();
}

componentDidUpdate(prevProps: IPkgGraphProps): void {
if (this.props.package !== prevProps.package) {
this._updatePackages();
}
}

private async _updatePackages(): Promise<void> {
try {
const available = await this.props.pkgManager.getDependencies(
this.props.package,
true
);
const data: GraphData<GraphNode, GraphLink> = { nodes: [], links: [] };
let error: React.ReactNode = null;
if (available[this.props.package] === null) {
// Manager does not support dependency query
error = (
<span>
Please install{' '}
<a
style={{ textDecoration: 'underline' }}
href="https://github.com/mamba-org/mamba"
rel="noreferrer"
target="_blank"
>
mamba
</a>{' '}
manager to resolve dependencies.
</span>
);
} else {
Object.keys(available).forEach(key => {
if (key === this.props.package) {
data.nodes.push({ id: key, color: 'orange' });
} else {
data.nodes.push({ id: key });
}

available[key].forEach(dep => {
const dependencie = dep.split(' ')[0];
if (!data.nodes.find(value => value.id === dependencie)) {
data.nodes.push({ id: dependencie });
}
data.links.push({ source: key, target: dependencie });
});
});
if (data.nodes.length === 0) {
error = <span>This is a pip package</span>;
}
}
this.setState({ data, error });
} catch (error) {
if (error.message !== 'cancelled') {
console.error('Error when looking for dependencies.', error);
}
}
}

render(): JSX.Element {
return (
<div>
{this.state.data === null ? (
<span>Loading dependencies</span>
) : (
<div>
{this.state.error || (
<Graph
id="graph-id"
data={this.state.data}
config={this.props.config}
/>
)}
</div>
)}
</div>
);
}
}

export class PkgGraphWidget extends ReactWidget {
constructor(pkgManager: Conda.IPackageManager, pkg: string) {
super();
this._package = pkg;
this._pkgManager = pkgManager;
}

render(): JSX.Element {
return <PkgGraph package={this._package} pkgManager={this._pkgManager} />;
}

private _pkgManager: Conda.IPackageManager;
private _package: string;
}
Loading

0 comments on commit 4b8378f

Please sign in to comment.