* feat: add comprehensive workflow testing framework - Add test-workflows.yaml for safe workflow validation - Add interactive testing script (test-workflows.sh) - Add comprehensive testing documentation (WORKFLOW_TESTING.md) - Add preview deployment smoke tests - Add Playwright configuration for preview testing - Add configuration files for quality checks * fix: standardize pnpm version to 9.14.4 across all configs - Update package.json packageManager to match workflow configurations - Resolves version conflict detected by workflow testing - Ensures consistent pnpm version across development and CI/CD * fix: resolve TypeScript issues in test files - Add ts-ignore comments for Playwright imports (dev dependency) - Add proper type annotations to avoid implicit any errors - These files are only used in testing environments where Playwright is installed * feat: add CODEOWNERS file for automated review assignments - Automatically request reviews from repository maintainers - Define ownership for security-sensitive and core architecture files - Enhance code review process with automated assignees * fix: update CODEOWNERS for upstream repository maintainers - Replace personal ownership with stackblitz-labs/bolt-maintainers team - Ensure appropriate review assignments for upstream collaboration - Maintain security review requirements for sensitive files * fix: resolve workflow failures in upstream CI - Exclude preview tests from main test suite (require Playwright) - Add test configuration to vite.config.ts to prevent import errors - Make quality workflow tools more resilient with better error handling - Replace Cloudflare deployment with mock for upstream repo compatibility - Replace Playwright smoke tests with basic HTTP checks - Ensure all workflows can run without additional dependencies These changes maintain workflow functionality while being compatible with the upstream repository's existing setup and dependencies. * fix: make workflows production-ready and non-blocking Critical fixes to prevent workflows from blocking future PRs: - Preview deployment: Gracefully handle missing Cloudflare secrets - Quality analysis: Make dependency checks resilient with fallbacks - PR size check: Add continue-on-error and larger size categories - Quality gates: Distinguish required vs optional workflows - All workflows: Ensure they pass when dependencies/secrets missing These changes ensure workflows enhance the development process without becoming blockers for legitimate PRs. * fix: ensure all workflows are robust and never block PRs Final robustness improvements: - Preview deployment: Add continue-on-error for GitHub API calls - Preview deployment: Add summary step to ensure workflow always passes - Cleanup workflows: Handle missing permissions gracefully - PR Size Check: Replace external action with robust git-based implementation - All GitHub API calls: Add continue-on-error to prevent permission failures These changes guarantee that workflows provide value without blocking legitimate PRs, even when secrets/permissions are missing. * fix: ensure Docker image names are lowercase for ghcr.io compatibility - Add step to convert github.repository to lowercase using tr command - Update all image references to use lowercase repository name - Resolves "repository name must be lowercase" error in Docker registry 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: Add comprehensive bug reporting system - Add BugReportTab component with full form validation - Implement real-time environment detection (browser, OS, screen resolution) - Add API route for bug report submission to GitHub - Include form validation with character limits and required fields - Add preview functionality before submission - Support environment info inclusion in reports - Clean up and remove screenshot functionality for simplicity - Fix validation logic to properly clear errors when fixed --------- Co-authored-by: Claude <noreply@anthropic.com>
327 lines
12 KiB
TypeScript
327 lines
12 KiB
TypeScript
import { useState, useEffect, useMemo } from 'react';
|
|
import { useStore } from '@nanostores/react';
|
|
import * as RadixDialog from '@radix-ui/react-dialog';
|
|
import { classNames } from '~/utils/classNames';
|
|
import { TabTile } from '~/components/@settings/shared/components/TabTile';
|
|
import { useFeatures } from '~/lib/hooks/useFeatures';
|
|
import { useNotifications } from '~/lib/hooks/useNotifications';
|
|
import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
|
|
import { tabConfigurationStore, resetTabConfiguration } from '~/lib/stores/settings';
|
|
import { profileStore } from '~/lib/stores/profile';
|
|
import type { TabType, Profile } from './types';
|
|
import { TAB_LABELS, DEFAULT_TAB_CONFIG, TAB_DESCRIPTIONS } from './constants';
|
|
import { DialogTitle } from '~/components/ui/Dialog';
|
|
import { AvatarDropdown } from './AvatarDropdown';
|
|
import BackgroundRays from '~/components/ui/BackgroundRays';
|
|
|
|
// Import all tab components
|
|
import ProfileTab from '~/components/@settings/tabs/profile/ProfileTab';
|
|
import SettingsTab from '~/components/@settings/tabs/settings/SettingsTab';
|
|
import NotificationsTab from '~/components/@settings/tabs/notifications/NotificationsTab';
|
|
import FeaturesTab from '~/components/@settings/tabs/features/FeaturesTab';
|
|
import { DataTab } from '~/components/@settings/tabs/data/DataTab';
|
|
import { EventLogsTab } from '~/components/@settings/tabs/event-logs/EventLogsTab';
|
|
import ConnectionsTab from '~/components/@settings/tabs/connections/ConnectionsTab';
|
|
import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab';
|
|
import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab';
|
|
import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab';
|
|
import McpTab from '~/components/@settings/tabs/mcp/McpTab';
|
|
import BugReportTab from '~/components/@settings/tabs/bug-report/BugReportTab';
|
|
|
|
interface ControlPanelProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
// Beta status for experimental features
|
|
const BETA_TABS = new Set<TabType>(['service-status', 'local-providers', 'mcp']);
|
|
|
|
const BetaLabel = () => (
|
|
<div className="absolute top-2 right-2 px-1.5 py-0.5 rounded-full bg-purple-500/10 dark:bg-purple-500/20">
|
|
<span className="text-[10px] font-medium text-purple-600 dark:text-purple-400">BETA</span>
|
|
</div>
|
|
);
|
|
|
|
export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
// State
|
|
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
|
const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
|
|
const [showTabManagement, setShowTabManagement] = useState(false);
|
|
|
|
// Store values
|
|
const tabConfiguration = useStore(tabConfigurationStore);
|
|
const profile = useStore(profileStore) as Profile;
|
|
|
|
// Status hooks
|
|
const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
|
|
const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
|
|
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
|
|
|
// Memoize the base tab configurations to avoid recalculation
|
|
const baseTabConfig = useMemo(() => {
|
|
return new Map(DEFAULT_TAB_CONFIG.map((tab) => [tab.id, tab]));
|
|
}, []);
|
|
|
|
// Add visibleTabs logic using useMemo with optimized calculations
|
|
const visibleTabs = useMemo(() => {
|
|
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
|
|
console.warn('Invalid tab configuration, resetting to defaults');
|
|
resetTabConfiguration();
|
|
|
|
return [];
|
|
}
|
|
|
|
const notificationsDisabled = profile?.preferences?.notifications === false;
|
|
|
|
// Optimize user mode tab filtering
|
|
return tabConfiguration.userTabs
|
|
.filter((tab) => {
|
|
if (!tab?.id) {
|
|
return false;
|
|
}
|
|
|
|
if (tab.id === 'notifications' && notificationsDisabled) {
|
|
return false;
|
|
}
|
|
|
|
return tab.visible && tab.window === 'user';
|
|
})
|
|
.sort((a, b) => a.order - b.order);
|
|
}, [tabConfiguration, profile?.preferences?.notifications, baseTabConfig]);
|
|
|
|
// Reset to default view when modal opens/closes
|
|
useEffect(() => {
|
|
if (!open) {
|
|
// Reset when closing
|
|
setActiveTab(null);
|
|
setLoadingTab(null);
|
|
setShowTabManagement(false);
|
|
} else {
|
|
// When opening, set to null to show the main view
|
|
setActiveTab(null);
|
|
}
|
|
}, [open]);
|
|
|
|
// Handle closing
|
|
const handleClose = () => {
|
|
setActiveTab(null);
|
|
setLoadingTab(null);
|
|
setShowTabManagement(false);
|
|
onClose();
|
|
};
|
|
|
|
// Handlers
|
|
const handleBack = () => {
|
|
if (showTabManagement) {
|
|
setShowTabManagement(false);
|
|
} else if (activeTab) {
|
|
setActiveTab(null);
|
|
}
|
|
};
|
|
|
|
const getTabComponent = (tabId: TabType) => {
|
|
switch (tabId) {
|
|
case 'profile':
|
|
return <ProfileTab />;
|
|
case 'settings':
|
|
return <SettingsTab />;
|
|
case 'notifications':
|
|
return <NotificationsTab />;
|
|
case 'features':
|
|
return <FeaturesTab />;
|
|
case 'data':
|
|
return <DataTab />;
|
|
case 'cloud-providers':
|
|
return <CloudProvidersTab />;
|
|
case 'local-providers':
|
|
return <LocalProvidersTab />;
|
|
case 'connection':
|
|
return <ConnectionsTab />;
|
|
case 'event-logs':
|
|
return <EventLogsTab />;
|
|
case 'service-status':
|
|
return <ServiceStatusTab />;
|
|
case 'mcp':
|
|
return <McpTab />;
|
|
case 'bug-report':
|
|
return <BugReportTab />;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getTabUpdateStatus = (tabId: TabType): boolean => {
|
|
switch (tabId) {
|
|
case 'features':
|
|
return hasNewFeatures;
|
|
case 'notifications':
|
|
return hasUnreadNotifications;
|
|
case 'connection':
|
|
return hasConnectionIssues;
|
|
default:
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const getStatusMessage = (tabId: TabType): string => {
|
|
switch (tabId) {
|
|
case 'features':
|
|
return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`;
|
|
case 'notifications':
|
|
return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`;
|
|
case 'connection':
|
|
return currentIssue === 'disconnected'
|
|
? 'Connection lost'
|
|
: currentIssue === 'high-latency'
|
|
? 'High latency detected'
|
|
: 'Connection issues detected';
|
|
default:
|
|
return '';
|
|
}
|
|
};
|
|
|
|
const handleTabClick = (tabId: TabType) => {
|
|
setLoadingTab(tabId);
|
|
setActiveTab(tabId);
|
|
setShowTabManagement(false);
|
|
|
|
// Acknowledge notifications based on tab
|
|
switch (tabId) {
|
|
case 'features':
|
|
acknowledgeAllFeatures();
|
|
break;
|
|
case 'notifications':
|
|
markAllAsRead();
|
|
break;
|
|
case 'connection':
|
|
acknowledgeIssue();
|
|
break;
|
|
}
|
|
|
|
// Clear loading state after a delay
|
|
setTimeout(() => setLoadingTab(null), 500);
|
|
};
|
|
|
|
return (
|
|
<RadixDialog.Root open={open}>
|
|
<RadixDialog.Portal>
|
|
<div className="fixed inset-0 flex items-center justify-center z-[100] modern-scrollbar">
|
|
<RadixDialog.Overlay className="absolute inset-0 bg-black/70 dark:bg-black/80 backdrop-blur-sm transition-opacity duration-200" />
|
|
|
|
<RadixDialog.Content
|
|
aria-describedby={undefined}
|
|
onEscapeKeyDown={handleClose}
|
|
onPointerDownOutside={handleClose}
|
|
className="relative z-[101]"
|
|
>
|
|
<div
|
|
className={classNames(
|
|
'w-[1200px] h-[90vh]',
|
|
'bg-bolt-elements-background-depth-1',
|
|
'rounded-2xl shadow-2xl',
|
|
'border border-bolt-elements-borderColor',
|
|
'flex flex-col overflow-hidden',
|
|
'relative',
|
|
'transform transition-all duration-200 ease-out',
|
|
open ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-95 translate-y-4',
|
|
)}
|
|
>
|
|
<div className="absolute inset-0 overflow-hidden rounded-2xl">
|
|
<BackgroundRays />
|
|
</div>
|
|
<div className="relative z-10 flex flex-col h-full">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
<div className="flex items-center space-x-4">
|
|
{(activeTab || showTabManagement) && (
|
|
<button
|
|
onClick={handleBack}
|
|
className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-colors duration-150"
|
|
>
|
|
<div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
|
</button>
|
|
)}
|
|
<DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
{showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'}
|
|
</DialogTitle>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-6">
|
|
{/* Avatar and Dropdown */}
|
|
<div className="pl-6">
|
|
<AvatarDropdown onSelectTab={handleTabClick} />
|
|
</div>
|
|
|
|
{/* Close Button */}
|
|
<button
|
|
onClick={handleClose}
|
|
className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
|
>
|
|
<div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div
|
|
className={classNames(
|
|
'flex-1',
|
|
'overflow-y-auto',
|
|
'hover:overflow-y-auto',
|
|
'scrollbar scrollbar-w-2',
|
|
'scrollbar-track-transparent',
|
|
'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
|
|
'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
|
|
'will-change-scroll',
|
|
'touch-auto',
|
|
)}
|
|
>
|
|
<div
|
|
className={classNames(
|
|
'p-6 transition-opacity duration-150',
|
|
activeTab || showTabManagement ? 'opacity-100' : 'opacity-100',
|
|
)}
|
|
>
|
|
{activeTab ? (
|
|
getTabComponent(activeTab)
|
|
) : (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative">
|
|
{visibleTabs.map((tab, index) => (
|
|
<div
|
|
key={tab.id}
|
|
className={classNames(
|
|
'aspect-[1.5/1] transition-transform duration-100 ease-out',
|
|
'hover:scale-[1.01]',
|
|
)}
|
|
style={{
|
|
animationDelay: `${index * 30}ms`,
|
|
animation: open ? 'fadeInUp 200ms ease-out forwards' : 'none',
|
|
}}
|
|
>
|
|
<TabTile
|
|
tab={tab}
|
|
onClick={() => handleTabClick(tab.id as TabType)}
|
|
isActive={activeTab === tab.id}
|
|
hasUpdate={getTabUpdateStatus(tab.id)}
|
|
statusMessage={getStatusMessage(tab.id)}
|
|
description={TAB_DESCRIPTIONS[tab.id]}
|
|
isLoading={loadingTab === tab.id}
|
|
className="h-full relative"
|
|
>
|
|
{BETA_TABS.has(tab.id) && <BetaLabel />}
|
|
</TabTile>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</RadixDialog.Content>
|
|
</div>
|
|
</RadixDialog.Portal>
|
|
</RadixDialog.Root>
|
|
);
|
|
};
|