diff --git a/binder/postBuild b/binder/postBuild
index 6981ad97..1b67e4c6 100644
--- a/binder/postBuild
+++ b/binder/postBuild
@@ -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
\ No newline at end of file
+xircuits list
diff --git a/src/context-menu/TrayContextMenu.tsx b/src/context-menu/TrayContextMenu.tsx
index 22eec809..7a185d70 100644
--- a/src/context-menu/TrayContextMenu.tsx
+++ b/src/context-menu/TrayContextMenu.tsx
@@ -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}`);
}
};
@@ -110,10 +151,29 @@ const TrayContextMenu = ({ app, x, y, visible, val, onClose }: TrayContextMenuPr
return ReactDOM.createPortal(
-
{ handleInstall(val); onClose(); }}>Install
-
{ handleShowInFileBrowser(val); onClose(); }}>Show in File Explorer
-
{ handleShowReadme(val); onClose(); }}>See Readme
-
{ handleShowExample(val); onClose(); }}>Show Example
+ {status === 'remote' ? (
+ <>
+
{ handleInstall(libraryName, refreshTrigger); onClose(); }}>Install
+ {validOptions.showPageInNewTab && (
+
{ handleShowPageInNewTab(libraryName); onClose(); }}>Open Repository
+ )}
+ >
+ ) : (
+ <>
+ {validOptions.showInFileBrowser && (
+
{ handleShowInFileBrowser(libraryName); onClose(); }}>Show in File Explorer
+ )}
+ {validOptions.showReadme && (
+
{ handleShowReadme(libraryName); onClose(); }}>See Readme
+ )}
+ {validOptions.showExample && (
+
{ handleShowExample(libraryName); onClose(); }}>Show Example
+ )}
+ {validOptions.showPageInNewTab && (
+
{ handleShowPageInNewTab(libraryName); onClose(); }}>Open Repository
+ )}
+ >
+ )}
,
document.body
);
diff --git a/src/tray_library/AdvanceComponentLib.tsx b/src/tray_library/AdvanceComponentLib.tsx
index 5013e8ec..665e787a 100644
--- a/src/tray_library/AdvanceComponentLib.tsx
+++ b/src/tray_library/AdvanceComponentLib.tsx
@@ -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);
diff --git a/src/tray_library/ComponentLibraryConfig.tsx b/src/tray_library/ComponentLibraryConfig.tsx
new file mode 100644
index 00000000..f42029fa
--- /dev/null
+++ b/src/tray_library/ComponentLibraryConfig.tsx
@@ -0,0 +1,77 @@
+import { requestAPI } from "../server/handler";
+
+let libraryConfigCache = {
+ data: null
+};
+
+export async function fetchComponentLibraryConfig() {
+ try {
+ const response = await requestAPI('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;
+};
diff --git a/src/tray_library/Sidebar.tsx b/src/tray_library/Sidebar.tsx
index e976bf1f..0d890d3e 100644
--- a/src/tray_library/Sidebar.tsx
+++ b/src/tray_library/Sidebar.tsx
@@ -4,7 +4,6 @@ import styled from '@emotion/styled';
import { TrayItemWidget } from './TrayItemWidget';
import { TrayWidget } from './TrayWidget';
import { JupyterFrontEnd } from '@jupyterlab/application';
-import { requestAPI } from '../server/handler';
import {
Accordion,
@@ -18,6 +17,7 @@ import { XircuitsFactory } from '../XircuitsFactory';
import TrayContextMenu from '../context-menu/TrayContextMenu';
import '../../style/ContextMenu.css'
+import { ComponentLibraryConfig, refreshComponentLibraryConfigCache } from './ComponentLibraryConfig';
export const Body = styled.div`
flex-grow: 1;
@@ -88,6 +88,7 @@ export default function Sidebar(props: SidebarProps) {
const [componentList, setComponentList] = React.useState([]);
const [category, setCategory] = React.useState([]);
+ const [remoteLibList, setRemoteLibList] = React.useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [runOnce, setRunOnce] = useState(false);
@@ -103,12 +104,7 @@ export default function Sidebar(props: SidebarProps) {
const fetchComponentList = async () => {
- try {
- // Trigger the load library config on refresh
- await requestAPI('library/reload_config', { method: 'POST', headers: { 'Content-Type': 'application/json' } });
- } catch (error){
- console.error('Failed to reload config: ', error);
- }
+ await refreshComponentLibraryConfigCache();
// get the component list
const component_list = await ComponentList();
@@ -124,6 +120,10 @@ export default function Sidebar(props: SidebarProps) {
setComponentList(component_list);
setCategory(component_library_name);
+
+ const libraryConfig = await ComponentLibraryConfig();
+ const remoteLibraries = libraryConfig.filter(library => library.status === "remote");
+ setRemoteLibList(remoteLibraries);
}
useEffect(() => {
@@ -135,7 +135,7 @@ export default function Sidebar(props: SidebarProps) {
}, [category, componentList]);
function handleRefreshOnClick() {
- refreshComponentListCache()
+ refreshComponentListCache();
fetchComponentList();
}
@@ -183,33 +183,55 @@ export default function Sidebar(props: SidebarProps) {
// Function to map categories
const mapCategories = (categories, components) => {
- return categories.map((val, i) => (
+ return categories.map((libraryName, i) => (
- handleRightClick(e, val["task"])}>{val["task"]}
+ handleRightClick(event, libraryName["task"], 'installed')}>{libraryName["task"]}
- {mapComponents(components.filter(component => component["category"].toString().toUpperCase() === val["task"].toString()), "")}
+ {mapComponents(components.filter(component => component["category"].toString().toUpperCase() === libraryName["task"].toString()), "")}
));
}
+
+ const mapRemoteLibraries = () => {
+ const sortedRemoteLibList = remoteLibList.sort((a, b) => a.library_id.localeCompare(b.library_id));
+
+ return sortedRemoteLibList.map((lib, i) => (
+
+
+ handleRightClick(event, lib.library_id, 'remote')}>{lib.library_id}
+
+
+ ));
+ };
const [contextMenuState, setContextMenuState] = useState({
visible: false,
x: 0,
y: 0,
- val: null
+ libraryName: null,
+ status: 'installed'
});
- const handleRightClick = (e, val) => {
+ const handleRightClick = (e, libraryName, status) => {
e.preventDefault();
+
+ // Prevent context menu from appearing for GENERAL component library
+ if (libraryName === 'GENERAL') {
+ return;
+ }
+
const rect = e.target.getBoundingClientRect();
setContextMenuState({
visible: true,
x: rect.right,
y: rect.top,
- val: val
+ libraryName: libraryName,
+ status: status
});
};
@@ -223,15 +245,22 @@ export default function Sidebar(props: SidebarProps) {
{searchTerm === "" ? (
-
- {mapCategories(category, componentList)}
-
+ <>
+
+ {mapCategories(category, componentList)}
+
+
+
+ AVAILABLE FOR INSTALLATION
+
+ {mapRemoteLibraries()}
+
+ >
) : (
mapComponents(componentList, searchTerm)
)}
@@ -243,9 +272,11 @@ export default function Sidebar(props: SidebarProps) {
x={contextMenuState.x}
y={contextMenuState.y}
visible={contextMenuState.visible}
- val={contextMenuState.val}
+ libraryName={contextMenuState.libraryName}
+ status={contextMenuState.status}
+ refreshTrigger={handleRefreshOnClick}
onClose={closeContextMenu}
/>