459 lines
16 KiB
Python
459 lines
16 KiB
Python
"""
|
|
Dashboard login and startup window for the Cluster4NPU UI application.
|
|
|
|
This module provides the main entry point window that allows users to create
|
|
new pipelines or load existing ones. It serves as the application launcher
|
|
and recent files manager.
|
|
|
|
Main Components:
|
|
- DashboardLogin: Main startup window with project management
|
|
- Recent files management and display
|
|
- New pipeline creation workflow
|
|
- Application navigation and routing
|
|
|
|
Usage:
|
|
from cluster4npu_ui.ui.windows.login import DashboardLogin
|
|
|
|
dashboard = DashboardLogin()
|
|
dashboard.show()
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from PyQt5.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
|
QListWidget, QListWidgetItem, QMessageBox, QFileDialog,
|
|
QFrame, QSizePolicy, QSpacerItem
|
|
)
|
|
from PyQt5.QtCore import Qt, pyqtSignal
|
|
from PyQt5.QtGui import QFont, QPixmap, QIcon
|
|
|
|
from cluster4npu_ui.config.settings import get_settings
|
|
|
|
|
|
class DashboardLogin(QWidget):
|
|
"""
|
|
Main startup window for the Cluster4NPU application.
|
|
|
|
Provides options to create new pipelines, load existing ones, and manage
|
|
recent files. Serves as the application's main entry point.
|
|
"""
|
|
|
|
# Signals
|
|
pipeline_requested = pyqtSignal(str) # Emitted when user wants to open/create pipeline
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.settings = get_settings()
|
|
self.setup_ui()
|
|
self.load_recent_files()
|
|
|
|
# Connect to integrated dashboard (will be implemented)
|
|
self.dashboard_window = None
|
|
|
|
def setup_ui(self):
|
|
"""Initialize the user interface."""
|
|
self.setWindowTitle("Cluster4NPU - Pipeline Dashboard")
|
|
self.setMinimumSize(800, 600)
|
|
self.resize(1000, 700)
|
|
|
|
# Main layout
|
|
main_layout = QVBoxLayout(self)
|
|
main_layout.setSpacing(20)
|
|
main_layout.setContentsMargins(40, 40, 40, 40)
|
|
|
|
# Header section
|
|
self.create_header(main_layout)
|
|
|
|
# Content section
|
|
content_layout = QHBoxLayout()
|
|
content_layout.setSpacing(30)
|
|
|
|
# Left side - Actions
|
|
self.create_actions_panel(content_layout)
|
|
|
|
# Right side - Recent files
|
|
self.create_recent_files_panel(content_layout)
|
|
|
|
main_layout.addLayout(content_layout)
|
|
|
|
# Footer
|
|
self.create_footer(main_layout)
|
|
|
|
def create_header(self, parent_layout):
|
|
"""Create the header section with title and description."""
|
|
header_frame = QFrame()
|
|
header_frame.setStyleSheet("""
|
|
QFrame {
|
|
background-color: #313244;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
}
|
|
""")
|
|
header_layout = QVBoxLayout(header_frame)
|
|
|
|
# Title
|
|
title_label = QLabel("Cluster4NPU Pipeline Designer")
|
|
title_label.setFont(QFont("Arial", 24, QFont.Bold))
|
|
title_label.setStyleSheet("color: #89b4fa; margin-bottom: 10px;")
|
|
title_label.setAlignment(Qt.AlignCenter)
|
|
header_layout.addWidget(title_label)
|
|
|
|
# Subtitle
|
|
subtitle_label = QLabel("Design, configure, and deploy high-performance ML inference pipelines")
|
|
subtitle_label.setFont(QFont("Arial", 14))
|
|
subtitle_label.setStyleSheet("color: #cdd6f4; margin-bottom: 5px;")
|
|
subtitle_label.setAlignment(Qt.AlignCenter)
|
|
header_layout.addWidget(subtitle_label)
|
|
|
|
# Version info
|
|
version_label = QLabel("Version 1.0.0 - Multi-stage NPU Pipeline System")
|
|
version_label.setFont(QFont("Arial", 10))
|
|
version_label.setStyleSheet("color: #6c7086;")
|
|
version_label.setAlignment(Qt.AlignCenter)
|
|
header_layout.addWidget(version_label)
|
|
|
|
parent_layout.addWidget(header_frame)
|
|
|
|
def create_actions_panel(self, parent_layout):
|
|
"""Create the actions panel with main buttons."""
|
|
actions_frame = QFrame()
|
|
actions_frame.setStyleSheet("""
|
|
QFrame {
|
|
background-color: #313244;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
}
|
|
""")
|
|
actions_frame.setMaximumWidth(350)
|
|
actions_layout = QVBoxLayout(actions_frame)
|
|
|
|
# Panel title
|
|
actions_title = QLabel("Get Started")
|
|
actions_title.setFont(QFont("Arial", 16, QFont.Bold))
|
|
actions_title.setStyleSheet("color: #f9e2af; margin-bottom: 20px;")
|
|
actions_layout.addWidget(actions_title)
|
|
|
|
# Create new pipeline button
|
|
self.new_pipeline_btn = QPushButton("Create New Pipeline")
|
|
self.new_pipeline_btn.setFont(QFont("Arial", 12, QFont.Bold))
|
|
self.new_pipeline_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #89b4fa, stop:1 #74c7ec);
|
|
color: #1e1e2e;
|
|
border: none;
|
|
padding: 15px 20px;
|
|
border-radius: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
QPushButton:hover {
|
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #a6c8ff, stop:1 #89dceb);
|
|
}
|
|
""")
|
|
self.new_pipeline_btn.clicked.connect(self.create_new_pipeline)
|
|
actions_layout.addWidget(self.new_pipeline_btn)
|
|
|
|
# Open existing pipeline button
|
|
self.open_pipeline_btn = QPushButton("Open Existing Pipeline")
|
|
self.open_pipeline_btn.setFont(QFont("Arial", 12))
|
|
self.open_pipeline_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #45475a;
|
|
color: #cdd6f4;
|
|
border: 2px solid #585b70;
|
|
padding: 15px 20px;
|
|
border-radius: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #585b70;
|
|
border-color: #89b4fa;
|
|
}
|
|
""")
|
|
self.open_pipeline_btn.clicked.connect(self.open_existing_pipeline)
|
|
actions_layout.addWidget(self.open_pipeline_btn)
|
|
|
|
# Import from template button
|
|
# self.import_template_btn = QPushButton("Import from Template")
|
|
# self.import_template_btn.setFont(QFont("Arial", 12))
|
|
# self.import_template_btn.setStyleSheet("""
|
|
# QPushButton {
|
|
# background-color: #45475a;
|
|
# color: #cdd6f4;
|
|
# border: 2px solid #585b70;
|
|
# padding: 15px 20px;
|
|
# border-radius: 10px;
|
|
# margin-bottom: 20px;
|
|
# }
|
|
# QPushButton:hover {
|
|
# background-color: #585b70;
|
|
# border-color: #a6e3a1;
|
|
# }
|
|
# """)
|
|
# self.import_template_btn.clicked.connect(self.import_template)
|
|
# actions_layout.addWidget(self.import_template_btn)
|
|
|
|
# Additional info
|
|
# info_label = QLabel("Start by creating a new pipeline or opening an existing .mflow file")
|
|
# info_label.setFont(QFont("Arial", 10))
|
|
# info_label.setStyleSheet("color: #6c7086; padding: 10px; background-color: #45475a; border-radius: 8px;")
|
|
# info_label.setWordWrap(True)
|
|
# actions_layout.addWidget(info_label)
|
|
|
|
# Spacer
|
|
actions_layout.addItem(QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding))
|
|
|
|
parent_layout.addWidget(actions_frame)
|
|
|
|
def create_recent_files_panel(self, parent_layout):
|
|
"""Create the recent files panel."""
|
|
recent_frame = QFrame()
|
|
recent_frame.setStyleSheet("""
|
|
QFrame {
|
|
background-color: #313244;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
}
|
|
""")
|
|
recent_layout = QVBoxLayout(recent_frame)
|
|
|
|
# Panel title with clear button
|
|
title_layout = QHBoxLayout()
|
|
recent_title = QLabel("Recent Pipelines")
|
|
recent_title.setFont(QFont("Arial", 16, QFont.Bold))
|
|
recent_title.setStyleSheet("color: #f9e2af;")
|
|
title_layout.addWidget(recent_title)
|
|
|
|
title_layout.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
|
|
|
self.clear_recent_btn = QPushButton("Clear All")
|
|
self.clear_recent_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #f38ba8;
|
|
color: #1e1e2e;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
border-radius: 5px;
|
|
font-size: 10px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #f2d5de;
|
|
}
|
|
""")
|
|
self.clear_recent_btn.clicked.connect(self.clear_recent_files)
|
|
title_layout.addWidget(self.clear_recent_btn)
|
|
|
|
recent_layout.addLayout(title_layout)
|
|
|
|
# Recent files list
|
|
self.recent_files_list = QListWidget()
|
|
self.recent_files_list.setStyleSheet("""
|
|
QListWidget {
|
|
background-color: #1e1e2e;
|
|
border: 2px solid #45475a;
|
|
border-radius: 8px;
|
|
padding: 5px;
|
|
}
|
|
QListWidget::item {
|
|
padding: 10px;
|
|
border-bottom: 1px solid #45475a;
|
|
border-radius: 4px;
|
|
margin: 2px;
|
|
}
|
|
QListWidget::item:hover {
|
|
background-color: #383a59;
|
|
}
|
|
QListWidget::item:selected {
|
|
background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #89b4fa, stop:1 #74c7ec);
|
|
color: #1e1e2e;
|
|
}
|
|
""")
|
|
self.recent_files_list.itemDoubleClicked.connect(self.open_recent_file)
|
|
recent_layout.addWidget(self.recent_files_list)
|
|
|
|
parent_layout.addWidget(recent_frame)
|
|
|
|
def create_footer(self, parent_layout):
|
|
"""Create the footer with additional options."""
|
|
footer_layout = QHBoxLayout()
|
|
|
|
# Documentation link
|
|
docs_btn = QPushButton("Documentation")
|
|
docs_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: transparent;
|
|
color: #89b4fa;
|
|
border: none;
|
|
text-decoration: underline;
|
|
padding: 5px;
|
|
}
|
|
QPushButton:hover {
|
|
color: #a6c8ff;
|
|
}
|
|
""")
|
|
footer_layout.addWidget(docs_btn)
|
|
|
|
footer_layout.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
|
|
|
# Examples link
|
|
examples_btn = QPushButton("Examples")
|
|
examples_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: transparent;
|
|
color: #a6e3a1;
|
|
border: none;
|
|
text-decoration: underline;
|
|
padding: 5px;
|
|
}
|
|
QPushButton:hover {
|
|
color: #b3f5c0;
|
|
}
|
|
""")
|
|
footer_layout.addWidget(examples_btn)
|
|
|
|
footer_layout.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
|
|
|
# Settings link
|
|
settings_btn = QPushButton("Settings")
|
|
settings_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: transparent;
|
|
color: #f9e2af;
|
|
border: none;
|
|
text-decoration: underline;
|
|
padding: 5px;
|
|
}
|
|
QPushButton:hover {
|
|
color: #fdeaa7;
|
|
}
|
|
""")
|
|
footer_layout.addWidget(settings_btn)
|
|
|
|
parent_layout.addLayout(footer_layout)
|
|
|
|
def load_recent_files(self):
|
|
"""Load and display recent files."""
|
|
self.recent_files_list.clear()
|
|
recent_files = self.settings.get_recent_files()
|
|
|
|
if not recent_files:
|
|
item = QListWidgetItem("No recent files")
|
|
item.setFlags(Qt.NoItemFlags) # Make it non-selectable
|
|
item.setData(Qt.UserRole, None)
|
|
self.recent_files_list.addItem(item)
|
|
return
|
|
|
|
for file_path in recent_files:
|
|
if os.path.exists(file_path):
|
|
# Extract filename and directory
|
|
file_name = os.path.basename(file_path)
|
|
file_dir = os.path.dirname(file_path)
|
|
|
|
# Create list item
|
|
item_text = f"{file_name}\n{file_dir}"
|
|
item = QListWidgetItem(item_text)
|
|
item.setData(Qt.UserRole, file_path)
|
|
item.setToolTip(file_path)
|
|
self.recent_files_list.addItem(item)
|
|
else:
|
|
# Remove non-existent files
|
|
self.settings.remove_recent_file(file_path)
|
|
|
|
def create_new_pipeline(self):
|
|
"""Create a new pipeline."""
|
|
try:
|
|
# Import here to avoid circular imports
|
|
from cluster4npu_ui.ui.dialogs.create_pipeline import CreatePipelineDialog
|
|
|
|
dialog = CreatePipelineDialog(self)
|
|
if dialog.exec_() == dialog.Accepted:
|
|
project_info = dialog.get_project_info()
|
|
self.launch_pipeline_editor(project_info.get('name', 'Untitled'))
|
|
|
|
except ImportError:
|
|
# Fallback: directly launch editor
|
|
self.launch_pipeline_editor("New Pipeline")
|
|
|
|
def open_existing_pipeline(self):
|
|
"""Open an existing pipeline file."""
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
self,
|
|
"Open Pipeline File",
|
|
self.settings.get_default_project_location(),
|
|
"Pipeline files (*.mflow);;All files (*)"
|
|
)
|
|
|
|
if file_path:
|
|
self.settings.add_recent_file(file_path)
|
|
self.load_recent_files()
|
|
self.launch_pipeline_editor(file_path)
|
|
|
|
def open_recent_file(self, item: QListWidgetItem):
|
|
"""Open a recent file."""
|
|
file_path = item.data(Qt.UserRole)
|
|
if file_path and os.path.exists(file_path):
|
|
self.launch_pipeline_editor(file_path)
|
|
elif file_path:
|
|
QMessageBox.warning(self, "File Not Found", f"The file '{file_path}' could not be found.")
|
|
self.settings.remove_recent_file(file_path)
|
|
self.load_recent_files()
|
|
|
|
def import_template(self):
|
|
"""Import a pipeline from template."""
|
|
QMessageBox.information(
|
|
self,
|
|
"Import Template",
|
|
"Template import functionality will be available in a future version."
|
|
)
|
|
|
|
def clear_recent_files(self):
|
|
"""Clear all recent files."""
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Clear Recent Files",
|
|
"Are you sure you want to clear all recent files?",
|
|
QMessageBox.Yes | QMessageBox.No,
|
|
QMessageBox.No
|
|
)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
self.settings.clear_recent_files()
|
|
self.load_recent_files()
|
|
|
|
def launch_pipeline_editor(self, project_info):
|
|
"""Launch the main pipeline editor."""
|
|
try:
|
|
# Import here to avoid circular imports
|
|
from cluster4npu_ui.ui.windows.dashboard import IntegratedPipelineDashboard
|
|
|
|
self.dashboard_window = IntegratedPipelineDashboard()
|
|
|
|
# Load project if it's a file path
|
|
if isinstance(project_info, str) and os.path.exists(project_info):
|
|
# Load the pipeline file
|
|
try:
|
|
self.dashboard_window.load_pipeline_file(project_info)
|
|
except Exception as e:
|
|
QMessageBox.warning(
|
|
self,
|
|
"File Load Warning",
|
|
f"Could not load pipeline file: {e}\n\n"
|
|
"Opening with empty pipeline instead."
|
|
)
|
|
|
|
self.dashboard_window.show()
|
|
self.hide() # Hide the login window
|
|
|
|
except ImportError as e:
|
|
QMessageBox.critical(
|
|
self,
|
|
"Error",
|
|
f"Could not launch pipeline editor: {e}\n\n"
|
|
"Please ensure all required modules are available."
|
|
)
|
|
|
|
def closeEvent(self, event):
|
|
"""Handle window close event."""
|
|
# Save window geometry
|
|
self.settings.set_window_geometry(self.saveGeometry())
|
|
event.accept() |