Cluster/ui/windows/login.py
2025-07-17 17:04:56 +08:00

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()