bolt.diy/app/components/ui/Dialog.tsx
Stijnus b86fd63700
feat: bolt dyi datatab (#1570)
* Update DataTab.tsx

## API Key Import Fix

We identified and fixed an issue with the API key import functionality in the DataTab component. The problem was that API keys were being stored in localStorage instead of cookies, and the key format was being incorrectly processed.

### Changes Made:

1. **Updated `handleImportAPIKeys` function**:
   - Changed to store API keys in cookies instead of localStorage
   - Modified to use provider names directly as keys (e.g., "OpenAI", "Google")
   - Added logic to skip comment fields (keys starting with "_")
   - Added page reload after successful import to apply changes immediately

2. **Updated `handleDownloadTemplate` function**:
   - Changed template format to use provider names as keys
   - Added explanatory comment in the template
   - Removed URL-related keys that weren't being used properly

3. **Fixed template format**:
   - Template now uses the correct format with provider names as keys
   - Added support for all available providers including Hyperbolic

These changes ensure that when users download the template, fill it with their API keys, and import it back, the keys are properly stored in cookies with the correct format that the application expects.

* backwards compatible old import template

* Update the export / import settings

Settings Export/Import Improvements
We've completely redesigned the settings export and import functionality to ensure all application settings are properly backed up and restored:
Key Improvements
Comprehensive Export Format: Now captures ALL settings from both localStorage and cookies, organized into logical categories (core, providers, features, UI, connections, debug, updates)
Robust Import System: Automatically detects format version and handles both new and legacy formats with detailed error handling
Complete Settings Coverage: Properly exports and imports settings from ALL tabs including:
Local provider configurations (Ollama, LMStudio, etc.)
Cloud provider API keys (OpenAI, Anthropic, etc.)
Feature toggles and preferences
UI configurations and tab settings
Connection settings (GitHub, Netlify)
Debug configurations and logs
Technical Details
Added version tracking to export files for better compatibility
Implemented fallback mechanisms if primary import methods fail
Added detailed logging for troubleshooting import/export issues
Created helper functions for safer data handling
Maintained backward compatibility with older export formats

Feature Settings:
Feature flags and viewed features
Developer mode settings
Energy saver mode configurations
User Preferences:
User profile information
Theme settings
Tab configurations
Connection Settings:
Netlify connections
Git authentication credentials
Any other service connections
Debug and System Settings:
Debug flags and acknowledged issues
Error logs and event logs
Update settings and preferences

* Update DataTab.tsx

* Update GithubConnection.tsx

revert the code back as asked

* feat: enhance style to match the project

* feat:small improvements

* feat: add major improvements

* Update Dialog.tsx

* Delete DataTab.tsx.bak

* feat: small updates

* Update DataVisualization.tsx

* feat: dark mode fix
2025-03-29 20:43:07 +01:00

450 lines
12 KiB
TypeScript

import * as RadixDialog from '@radix-ui/react-dialog';
import { motion, type Variants } from 'framer-motion';
import React, { memo, type ReactNode, useState, useEffect } from 'react';
import { classNames } from '~/utils/classNames';
import { cubicEasingFn } from '~/utils/easings';
import { IconButton } from './IconButton';
import { Button } from './Button';
import { FixedSizeList } from 'react-window';
import { Checkbox } from './Checkbox';
import { Label } from './Label';
export { Close as DialogClose, Root as DialogRoot } from '@radix-ui/react-dialog';
interface DialogButtonProps {
type: 'primary' | 'secondary' | 'danger';
children: ReactNode;
onClick?: (event: React.MouseEvent) => void;
disabled?: boolean;
}
export const DialogButton = memo(({ type, children, onClick, disabled }: DialogButtonProps) => {
return (
<button
className={classNames(
'inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm transition-colors',
type === 'primary'
? 'bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-500 dark:hover:bg-purple-600'
: type === 'secondary'
? 'bg-transparent text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-100'
: 'bg-transparent text-red-500 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10',
)}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
});
export const DialogTitle = memo(({ className, children, ...props }: RadixDialog.DialogTitleProps) => {
return (
<RadixDialog.Title
className={classNames('text-lg font-medium text-bolt-elements-textPrimary flex items-center gap-2', className)}
{...props}
>
{children}
</RadixDialog.Title>
);
});
export const DialogDescription = memo(({ className, children, ...props }: RadixDialog.DialogDescriptionProps) => {
return (
<RadixDialog.Description
className={classNames('text-sm text-bolt-elements-textSecondary mt-1', className)}
{...props}
>
{children}
</RadixDialog.Description>
);
});
const transition = {
duration: 0.15,
ease: cubicEasingFn,
};
export const dialogBackdropVariants = {
closed: {
opacity: 0,
transition,
},
open: {
opacity: 1,
transition,
},
} satisfies Variants;
export const dialogVariants = {
closed: {
x: '-50%',
y: '-40%',
scale: 0.96,
opacity: 0,
transition,
},
open: {
x: '-50%',
y: '-50%',
scale: 1,
opacity: 1,
transition,
},
} satisfies Variants;
interface DialogProps {
children: ReactNode;
className?: string;
showCloseButton?: boolean;
onClose?: () => void;
onBackdrop?: () => void;
}
export const Dialog = memo(({ children, className, showCloseButton = true, onClose, onBackdrop }: DialogProps) => {
return (
<RadixDialog.Portal>
<RadixDialog.Overlay asChild>
<motion.div
className={classNames('fixed inset-0 z-[9999] bg-black/70 dark:bg-black/80 backdrop-blur-sm')}
initial="closed"
animate="open"
exit="closed"
variants={dialogBackdropVariants}
onClick={onBackdrop}
/>
</RadixDialog.Overlay>
<RadixDialog.Content asChild>
<motion.div
className={classNames(
'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-gray-950 rounded-lg shadow-xl border border-bolt-elements-borderColor z-[9999] w-[520px]',
className,
)}
initial="closed"
animate="open"
exit="closed"
variants={dialogVariants}
>
<div className="flex flex-col">
{children}
{showCloseButton && (
<RadixDialog.Close asChild onClick={onClose}>
<IconButton
icon="i-ph:x"
className="absolute top-3 right-3 text-bolt-elements-textTertiary hover:text-bolt-elements-textSecondary"
/>
</RadixDialog.Close>
)}
</div>
</motion.div>
</RadixDialog.Content>
</RadixDialog.Portal>
);
});
/**
* Props for the ConfirmationDialog component
*/
export interface ConfirmationDialogProps {
/**
* Whether the dialog is open
*/
isOpen: boolean;
/**
* Callback when the dialog is closed
*/
onClose: () => void;
/**
* Callback when the confirm button is clicked
*/
onConfirm: () => void;
/**
* The title of the dialog
*/
title: string;
/**
* The description of the dialog
*/
description: string;
/**
* The text for the confirm button
*/
confirmLabel?: string;
/**
* The text for the cancel button
*/
cancelLabel?: string;
/**
* The variant of the confirm button
*/
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
/**
* Whether the confirm button is in a loading state
*/
isLoading?: boolean;
}
/**
* A reusable confirmation dialog component that uses the Dialog component
*/
export function ConfirmationDialog({
isOpen,
onClose,
title,
description,
confirmLabel = 'Confirm',
cancelLabel = 'Cancel',
variant = 'default',
isLoading = false,
onConfirm,
}: ConfirmationDialogProps) {
return (
<RadixDialog.Root open={isOpen} onOpenChange={onClose}>
<Dialog showCloseButton={false}>
<div className="p-6 bg-white dark:bg-gray-950 relative z-10">
<DialogTitle>{title}</DialogTitle>
<DialogDescription className="mb-4">{description}</DialogDescription>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={onClose} disabled={isLoading}>
{cancelLabel}
</Button>
<Button
variant={variant}
onClick={onConfirm}
disabled={isLoading}
className={
variant === 'destructive'
? 'bg-red-500 text-white hover:bg-red-600'
: 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent hover:bg-bolt-elements-button-primary-backgroundHover'
}
>
{isLoading ? (
<>
<div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
{confirmLabel}
</>
) : (
confirmLabel
)}
</Button>
</div>
</div>
</Dialog>
</RadixDialog.Root>
);
}
/**
* Type for selection item in SelectionDialog
*/
type SelectionItem = {
id: string;
label: string;
description?: string;
};
/**
* Props for the SelectionDialog component
*/
export interface SelectionDialogProps {
/**
* The title of the dialog
*/
title: string;
/**
* The items to select from
*/
items: SelectionItem[];
/**
* Whether the dialog is open
*/
isOpen: boolean;
/**
* Callback when the dialog is closed
*/
onClose: () => void;
/**
* Callback when the confirm button is clicked with selected item IDs
*/
onConfirm: (selectedIds: string[]) => void;
/**
* The text for the confirm button
*/
confirmLabel?: string;
/**
* The maximum height of the selection list
*/
maxHeight?: string;
}
/**
* A reusable selection dialog component that uses the Dialog component
*/
export function SelectionDialog({
title,
items,
isOpen,
onClose,
onConfirm,
confirmLabel = 'Confirm',
maxHeight = '60vh',
}: SelectionDialogProps) {
const [selectedItems, setSelectedItems] = useState<string[]>([]);
const [selectAll, setSelectAll] = useState(false);
// Reset selected items when dialog opens
useEffect(() => {
if (isOpen) {
setSelectedItems([]);
setSelectAll(false);
}
}, [isOpen]);
const handleToggleItem = (id: string) => {
setSelectedItems((prev) => (prev.includes(id) ? prev.filter((itemId) => itemId !== id) : [...prev, id]));
};
const handleSelectAll = () => {
if (selectedItems.length === items.length) {
setSelectedItems([]);
setSelectAll(false);
} else {
setSelectedItems(items.map((item) => item.id));
setSelectAll(true);
}
};
const handleConfirm = () => {
onConfirm(selectedItems);
onClose();
};
// Calculate the height for the virtualized list
const listHeight = Math.min(
items.length * 60,
parseInt(maxHeight.replace('vh', '')) * window.innerHeight * 0.01 - 40,
);
// Render each item in the virtualized list
const ItemRenderer = ({ index, style }: { index: number; style: React.CSSProperties }) => {
const item = items[index];
return (
<div
key={item.id}
className={classNames(
'flex items-start space-x-3 p-2 rounded-md transition-colors',
selectedItems.includes(item.id)
? 'bg-bolt-elements-item-backgroundAccent'
: 'bg-bolt-elements-bg-depth-2 hover:bg-bolt-elements-item-backgroundActive',
)}
style={{
...style,
width: '100%',
boxSizing: 'border-box',
}}
>
<Checkbox
id={`item-${item.id}`}
checked={selectedItems.includes(item.id)}
onCheckedChange={() => handleToggleItem(item.id)}
/>
<div className="grid gap-1.5 leading-none">
<Label
htmlFor={`item-${item.id}`}
className={classNames(
'text-sm font-medium cursor-pointer',
selectedItems.includes(item.id)
? 'text-bolt-elements-item-contentAccent'
: 'text-bolt-elements-textPrimary',
)}
>
{item.label}
</Label>
{item.description && <p className="text-xs text-bolt-elements-textSecondary">{item.description}</p>}
</div>
</div>
);
};
return (
<RadixDialog.Root open={isOpen} onOpenChange={onClose}>
<Dialog showCloseButton={false}>
<div className="p-6 bg-white dark:bg-gray-950 relative z-10">
<DialogTitle>{title}</DialogTitle>
<DialogDescription className="mt-2 mb-4">
Select the items you want to include and click{' '}
<span className="text-bolt-elements-item-contentAccent font-medium">{confirmLabel}</span>.
</DialogDescription>
<div className="py-4">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-bolt-elements-textSecondary">
{selectedItems.length} of {items.length} selected
</span>
<Button
variant="ghost"
size="sm"
onClick={handleSelectAll}
className="text-xs h-8 px-2 text-bolt-elements-textPrimary hover:text-bolt-elements-item-contentAccent hover:bg-bolt-elements-item-backgroundAccent bg-bolt-elements-bg-depth-2 dark:bg-transparent"
>
{selectAll ? 'Deselect All' : 'Select All'}
</Button>
</div>
<div
className="pr-2 border rounded-md border-bolt-elements-borderColor bg-bolt-elements-bg-depth-2"
style={{
maxHeight,
}}
>
{items.length > 0 ? (
<FixedSizeList
height={listHeight}
width="100%"
itemCount={items.length}
itemSize={60}
className="scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-bolt-elements-bg-depth-3"
>
{ItemRenderer}
</FixedSizeList>
) : (
<div className="text-center py-4 text-sm text-bolt-elements-textTertiary">No items to display</div>
)}
</div>
</div>
<div className="flex justify-between mt-6">
<Button
variant="outline"
onClick={onClose}
className="border-bolt-elements-borderColor text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive"
>
Cancel
</Button>
<Button
onClick={handleConfirm}
disabled={selectedItems.length === 0}
className="bg-accent-500 text-white hover:bg-accent-600 disabled:opacity-50 disabled:pointer-events-none"
>
{confirmLabel}
</Button>
</div>
</div>
</Dialog>
</RadixDialog.Root>
);
}