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

✨ Display Remote Component Libraries #297

Merged
merged 11 commits into from
Feb 23, 2024
3 changes: 2 additions & 1 deletion binder/postBuild
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
find xai_components/ -mindepth 1 -maxdepth 1 -type d ! -name 'xai_controlflow' ! -name 'xai_events' ! -name 'xai_template' ! -name 'xai_utils' -exec rm -rf {} +
python -m pip install -U pip
pip install -e .
jupyter labextension develop . --overwrite
jupyter server extension enable xircuits
xircuits list
xircuits list
178 changes: 119 additions & 59 deletions src/context-menu/TrayContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,106 +1,147 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import ReactDOM from 'react-dom';
import { requestAPI } from '../server/handler';
import { commandIDs } from '../components/XircuitsBodyWidget';
import { startRunOutputStr } from '../components/runner/RunOutput';
import '../../style/ContextMenu.css';
import { buildLocalFilePath, fetchLibraryConfig } from '../tray_library/ComponentLibraryConfig';

export interface TrayContextMenuProps {
app: any; // Specify the correct type
app: any;
x: number;
y: number;
visible: boolean;
val: any; // Type this appropriately
libraryName: string;
status: string;
refreshTrigger: () => void;
onClose: () => void;
}

async function requestLibrary(libraryName, endpoint) {
const data = { libraryName };

try {
return await requestAPI(endpoint, {
body: JSON.stringify(data),
method: 'POST',
});
} catch (reason) {
console.error(`Error on POST /${endpoint}`, data, reason);
}
}

const TrayContextMenu = ({ app, x, y, visible, val, onClose }: TrayContextMenuProps) => {
// Ref for the context menu
const TrayContextMenu = ({ app, x, y, visible, libraryName, status, refreshTrigger, onClose }: TrayContextMenuProps) => {
const trayContextMenuRef = useRef(null);
const [validOptions, setValidOptions] = useState({
showInFileBrowser: false,
showReadme: false,
showExample: false,
showPageInNewTab: false
});

useEffect(() => {
// Initialize all options as invalid
setValidOptions({
showInFileBrowser: false,
showReadme: false,
showExample: false,
showPageInNewTab: false
});

const validateOptions = async () => {
try {
const libraryConfig = await fetchLibraryConfig(libraryName);
setValidOptions({
showInFileBrowser: !!libraryConfig.local_path,
showReadme: await buildLocalFilePath(libraryName, 'readme') !== null,
showExample: await buildLocalFilePath(libraryName, 'default_example_path') !== null,
showPageInNewTab: !!libraryConfig.repository
});
} catch (error) {
console.error('Error validating context menu options:', error);
}
};

if (visible) {
validateOptions();
}
}, [libraryName, visible]);

// Function to check if a click is outside the context menu
const handleClickOutside = (event) => {
if (event.target.className !== "context-menu-option") {
onClose();
}
};

// Effect for handling click outside
useEffect(() => {
document.addEventListener('click', handleClickOutside, true);
return () => {
document.removeEventListener('click', handleClickOutside, true);
};
}, []);

// Context menu action handlers
const handleInstall = async (val) => {
const userResponse = confirm("Do you want to proceed with " + val + " library installation?");
const handleInstall = async (libraryName, refreshTrigger) => {
const userResponse = confirm(`Do you want to proceed with ${libraryName} library installation?`);
if (userResponse) {
try {
const response = await requestLibrary(val, "library/get_directory");
if (response['path']) {
let code = startRunOutputStr()
code += "!pip install -r " + response['path'] + "/requirements.txt"
// clone the repository
const response: any = await requestAPI("library/fetch", {
body: JSON.stringify({libraryName}),
method: 'POST',
});

if (response.status !== 'OK') {
throw new Error(response.message || 'Failed to fetch the library.');
}

const libraryConfig = await fetchLibraryConfig(libraryName);
if (libraryConfig && libraryConfig.local_path) {
let code = startRunOutputStr();
code += `!pip install -r ${libraryConfig.local_path}/requirements.txt`;
app.commands.execute(commandIDs.executeToOutputPanel, { code });
console.log(`${val} library sucessfully installed.`);
} else if (response['message']) {
alert(response['message']);
console.log(`${libraryName} library successfully installed.`);
} else {
alert(`Library configuration not found for: ${libraryName}`);
}
refreshTrigger();
} catch (error) {
alert(`Failed to install ${val}: ` + error);
alert(`Failed to install ${libraryName}. Please check the console for more details.`);
console.error(`Failed to install ${libraryName}:`, error);
}
}
}
const handleShowInFileBrowser = async (val) => {
}
};

const handleShowInFileBrowser = async (libraryName) => {
try {
const response = await requestLibrary(val, "library/get_directory");
if (response['path']) {
await app.commands.execute('filebrowser:go-to-path', { path: response['path'] });
} else if (response['message']) {
alert(response['message']);
const libraryConfig = await fetchLibraryConfig(libraryName);

if (libraryConfig && libraryConfig.local_path) {
await app.commands.execute('filebrowser:go-to-path', { path: libraryConfig.local_path });
}
} catch (error) {
alert('Failed to Show in File Browser: ' + error);
alert(`Failed to Show in File Browser: ${error}`);
}
};

const handleShowReadme = async (val) => {

const handleShowReadme = async (libraryName) => {
try {
const readmePath = await buildLocalFilePath(libraryName, 'readme');
if (readmePath) {
await app.commands.execute('markdownviewer:open', { path: readmePath, options: { mode: 'split-right' } });
}
} catch (error) {
alert('Failed to Show Readme: ' + error);
}
};

const handleShowExample = async (libraryName) => {
try {
const response = await requestLibrary(val, "library/get_readme");
if (response['path']) {
await app.commands.execute('markdownviewer:open', { path: response['path'], options: { mode: 'split-right'} });
} else if (response['message']) {
alert(response['message']);
const examplePath = await buildLocalFilePath(libraryName, 'default_example_path');
if (examplePath) {
await app.commands.execute('docmanager:open', { path: examplePath });
}
} catch (error) {
alert('Failed to Show Readme: ' + error);
alert('Failed to Show Example: ' + error);
}
};

const handleShowExample = async (val) => {
const handleShowPageInNewTab = async (libraryName) => {
try {
const response = await requestLibrary(val, "library/get_example");
if (response['path']) {
await app.commands.execute('docmanager:open', { path: response['path'] });
} else if (response['message']) {
alert(response['message']);
const libraryConfig = await fetchLibraryConfig(libraryName);

if (libraryConfig && libraryConfig.repository) {
window.open(libraryConfig.repository, '_blank');
}
} catch (error) {
alert('Failed to Show Example: ' + error);
alert(`Failed to Open Page: ${error}`);
}
};

Expand All @@ -110,10 +151,29 @@ const TrayContextMenu = ({ app, x, y, visible, val, onClose }: TrayContextMenuPr

return ReactDOM.createPortal(
<div className="context-menu" ref={trayContextMenuRef} style={{ position: 'absolute', left: `${x+5}px`, top: `${y}px`, zIndex: 1000 }}>
<div className="context-menu-option" onClick={() => { handleInstall(val); onClose(); }}>Install</div>
<div className="context-menu-option" onClick={() => { handleShowInFileBrowser(val); onClose(); }}>Show in File Explorer</div>
<div className="context-menu-option" onClick={() => { handleShowReadme(val); onClose(); }}>See Readme</div>
<div className="context-menu-option" onClick={() => { handleShowExample(val); onClose(); }}>Show Example</div>
{status === 'remote' ? (
<>
<div className="context-menu-option" onClick={() => { handleInstall(libraryName, refreshTrigger); onClose(); }}>Install</div>
{validOptions.showPageInNewTab && (
<div className="context-menu-option" onClick={() => { handleShowPageInNewTab(libraryName); onClose(); }}>Open Repository</div>
)}
</>
) : (
<>
{validOptions.showInFileBrowser && (
<div className="context-menu-option" onClick={() => { handleShowInFileBrowser(libraryName); onClose(); }}>Show in File Explorer</div>
)}
{validOptions.showReadme && (
<div className="context-menu-option" onClick={() => { handleShowReadme(libraryName); onClose(); }}>See Readme</div>
)}
{validOptions.showExample && (
<div className="context-menu-option" onClick={() => { handleShowExample(libraryName); onClose(); }}>Show Example</div>
)}
{validOptions.showPageInNewTab && (
<div className="context-menu-option" onClick={() => { handleShowPageInNewTab(libraryName); onClose(); }}>Open Repository</div>
)}
</>
)}
</div>,
document.body
);
Expand Down
4 changes: 1 addition & 3 deletions src/tray_library/AdvanceComponentLib.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ interface AdvancedComponentLibraryProps {
export async function fetchNodeByName(name?: string) {
let componentList: string[] = [];

// get the component list
const response_1 = await ComponentList();
componentList = response_1;
componentList = await ComponentList();

let component_task = componentList.map(x => x["task"]);
let drop_node = component_task.indexOf(name);
Expand Down
77 changes: 77 additions & 0 deletions src/tray_library/ComponentLibraryConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { requestAPI } from "../server/handler";

let libraryConfigCache = {
data: null
};

export async function fetchComponentLibraryConfig() {
try {
const response = await requestAPI<any>('library/get_config');
if (response.status === 'OK' && response.config) {
return response.config.libraries;
} else {
console.error('Failed to fetch remote libraries due to unexpected response:', response);
return [];
}
} catch (error) {
console.error('Failed to fetch remote libraries:', error);
return [];
}
}

export async function reloadComponentLibraryConfig() {

try {
await requestAPI('library/reload_config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
} catch (error) {
console.error('Failed to reload config: ', error);
}
}

export async function ComponentLibraryConfig() {

if (!libraryConfigCache.data) {
libraryConfigCache.data = await fetchComponentLibraryConfig();
}

return libraryConfigCache.data;
}

export async function refreshComponentLibraryConfigCache() {
await reloadComponentLibraryConfig();
libraryConfigCache.data = await fetchComponentLibraryConfig();
}

export const fetchLibraryConfig = async (libName) => {
try {
let config = await ComponentLibraryConfig();
const libraryConfig = config.find(library => library.library_id === libName.toUpperCase());

if (!libraryConfig) {
// console.log(`Library not found for: ${libName}`);
return null;
}

return libraryConfig;
} catch (error) {
// console.log(`Failed to fetch library configuration: ${error}`);
return null;
}
};

export const buildLocalFilePath = async (libName, fileKey) => {
const libraryConfig = await fetchLibraryConfig(libName);

if (libraryConfig && libraryConfig[fileKey]) {
return `${libraryConfig.local_path}/${libraryConfig[fileKey]}`;
} else if (libraryConfig) {
// console.log(`File not found for: ${libName} (Key: ${fileKey})`);
}

return null;
};
Loading