fix: modals, settings, panel (#2187)

This commit is contained in:
Emir Karabeg
2025-12-04 12:01:36 -08:00
committed by GitHub
parent f44e7e34ec
commit 042de6a944
20 changed files with 275 additions and 153 deletions

View File

@@ -219,6 +219,7 @@ export function Chat() {
const [chatMessage, setChatMessage] = useState('')
const [promptHistory, setPromptHistory] = useState<string[]>([])
const [historyIndex, setHistoryIndex] = useState(-1)
const [moreMenuOpen, setMoreMenuOpen] = useState(false)
// Refs
const inputRef = useRef<HTMLInputElement>(null)
@@ -836,7 +837,7 @@ export function Chat() {
<div className='flex flex-shrink-0 items-center gap-[8px]'>
{/* More menu with actions */}
<Popover variant='default'>
<Popover variant='default' open={moreMenuOpen} onOpenChange={setMoreMenuOpen}>
<PopoverTrigger asChild>
<Button
variant='ghost'
@@ -858,6 +859,7 @@ export function Chat() {
onClick={(e) => {
e.stopPropagation()
if (activeWorkflowId) exportChatCSV(activeWorkflowId)
setMoreMenuOpen(false)
}}
disabled={workflowMessages.length === 0}
>
@@ -868,6 +870,7 @@ export function Chat() {
onClick={(e) => {
e.stopPropagation()
if (activeWorkflowId) clearChat(activeWorkflowId)
setMoreMenuOpen(false)
}}
disabled={workflowMessages.length === 0}
>

View File

@@ -780,7 +780,7 @@ function AuthSelector({
<Label className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'>
{authType === 'email' ? 'Allowed emails' : 'Allowed SSO emails'}
</Label>
<div className='scrollbar-hide flex max-h-32 min-h-9 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[6px] py-[4px] focus-within:outline-none dark:bg-[var(--surface-9)]'>
<div className='scrollbar-hide flex max-h-32 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[8px] py-[6px] focus-within:outline-none dark:bg-[var(--surface-9)]'>
{invalidEmails.map((email, index) => (
<EmailTag
key={`invalid-${index}`}
@@ -798,7 +798,7 @@ function AuthSelector({
disabled={disabled}
/>
))}
<Input
<input
type='text'
value={emailInputValue}
onChange={(e) => setEmailInputValue(e.target.value)}
@@ -810,10 +810,7 @@ function AuthSelector({
? 'Add another email'
: 'Enter emails or domains (@example.com)'
}
className={cn(
'h-6 min-w-[180px] flex-1 border-none bg-transparent p-0 text-[13px] focus-visible:ring-0 focus-visible:ring-offset-0',
emails.length > 0 || invalidEmails.length > 0 ? 'pl-[4px]' : 'pl-[4px]'
)}
className='min-w-[180px] flex-1 border-none bg-transparent p-0 font-medium font-sans text-foreground text-sm outline-none placeholder:text-[var(--text-muted)] disabled:cursor-not-allowed disabled:opacity-50'
disabled={disabled}
/>
</div>

View File

@@ -1,7 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
import { Loader2, Plus } from 'lucide-react'
import { Loader2 } from 'lucide-react'
import { Button, Combobox, Input, Label, Textarea } from '@/components/emcn'
import {
Modal,
@@ -118,7 +118,7 @@ export function TemplateDeploy({
setLoadingCreators(true)
try {
const response = await fetch('/api/creator-profiles')
const response = await fetch('/api/creators')
if (response.ok) {
const data = await response.json()
const profiles = (data.profiles || []).map((profile: any) => ({
@@ -321,24 +321,23 @@ export function TemplateDeploy({
{creatorOptions.length === 0 && !loadingCreators ? (
<Button
type='button'
variant='outline'
variant='primary'
onClick={() => {
try {
const event = new CustomEvent('open-settings', {
detail: { tab: 'creator-profile' },
detail: { tab: 'template-profile' },
})
window.dispatchEvent(event)
logger.info('Opened Settings modal at creator-profile section')
logger.info('Opened Settings modal at template-profile section')
} catch (error) {
logger.error('Failed to open Settings modal for creator profile', {
logger.error('Failed to open Settings modal for template profile', {
error,
})
}
}}
className='gap-[8px]'
>
<Plus className='h-[14px] w-[14px] text-[var(--text-muted)]' />
<span className='text-[var(--text-muted)]'>Create a Creator Profile</span>
<span>Create Template Profile</span>
</Button>
) : (
<Combobox

View File

@@ -1,15 +1,7 @@
'use client'
import { Check } from 'lucide-react'
import {
Button,
Modal,
ModalContent,
ModalDescription,
ModalFooter,
ModalHeader,
ModalTitle,
} from '@/components/emcn'
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
import { client } from '@/lib/auth/auth-client'
import { createLogger } from '@/lib/logs/console/logger'
import {
@@ -339,63 +331,63 @@ export function OAuthRequiredModal({
return (
<Modal open={isOpen} onOpenChange={(open) => !open && onClose()}>
<ModalContent className='sm:max-w-md'>
<ModalHeader>
<ModalTitle>Additional Access Required</ModalTitle>
<ModalDescription>
The "{toolName}" tool requires access to your {providerName} account to function
properly.
</ModalDescription>
</ModalHeader>
<div className='flex flex-col gap-4 py-4'>
<div className='flex items-center gap-4'>
<div className='rounded-full bg-muted p-2'>
<ProviderIcon className='h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium text-sm'>Connect {providerName}</p>
<p className='text-muted-foreground text-sm'>
You need to connect your {providerName} account to continue
</p>
</div>
</div>
{displayScopes.length > 0 && (
<div className='rounded-md border bg-muted/50'>
<div className='border-b px-4 py-3'>
<h4 className='font-medium text-sm'>Permissions requested</h4>
<ModalContent className='w-[460px]'>
<ModalHeader>Connect {providerName}</ModalHeader>
<ModalBody>
<div className='flex flex-col gap-[16px]'>
<div className='flex items-center gap-[14px]'>
<div className='flex h-[40px] w-[40px] flex-shrink-0 items-center justify-center rounded-[8px] bg-[var(--surface-6)]'>
<ProviderIcon className='h-[18px] w-[18px]' />
</div>
<div className='flex-1'>
<p className='font-medium text-[13px] text-[var(--text-primary)]'>
Connect your {providerName} account
</p>
<p className='text-[12px] text-[var(--text-tertiary)]'>
The "{toolName}" tool requires access to your account
</p>
</div>
<ul className='max-h-[400px] space-y-3 overflow-y-auto px-4 py-3'>
{displayScopes.map((scope) => (
<li key={scope} className='flex items-start gap-2 text-sm'>
<div className='mt-1 rounded-full bg-muted p-0.5'>
<Check className='h-3 w-3' />
</div>
<div className='text-muted-foreground'>
<span>{getScopeDescription(scope)}</span>
{newScopesSet.has(scope) && (
<span className='ml-2 rounded-[4px] border border-amber-500/30 bg-amber-500/10 px-1.5 py-0.5 text-[10px] text-amber-300'>
New
</span>
)}
</div>
</li>
))}
</ul>
</div>
)}
</div>
{displayScopes.length > 0 && (
<div className='rounded-[8px] border bg-[var(--surface-6)]'>
<div className='border-b px-[14px] py-[10px]'>
<h4 className='font-medium text-[13px] text-[var(--text-primary)]'>
Permissions requested
</h4>
</div>
<ul className='max-h-[330px] space-y-[10px] overflow-y-auto px-[14px] py-[12px]'>
{displayScopes.map((scope) => (
<li key={scope} className='flex items-start gap-[10px]'>
<div className='mt-[3px] flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
<Check className='h-[10px] w-[10px] text-[var(--text-primary)]' />
</div>
<div className='flex-1 text-[12px] text-[var(--text-primary)]'>
<span>{getScopeDescription(scope)}</span>
{newScopesSet.has(scope) && (
<span className='ml-[8px] rounded-[4px] border border-amber-500/30 bg-amber-500/10 px-[6px] py-[2px] text-[10px] text-amber-300'>
New
</span>
)}
</div>
</li>
))}
</ul>
</div>
)}
</div>
</ModalBody>
<ModalFooter>
<Button variant='outline' onClick={onClose} className='h-[32px] px-[12px]'>
<Button variant='active' onClick={onClose}>
Cancel
</Button>
<Button
variant='primary'
type='button'
onClick={handleConnectDirectly}
className='h-[32px] px-[12px]'
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
>
Connect Now
Connect
</Button>
</ModalFooter>
</ModalContent>

View File

@@ -166,18 +166,6 @@ export function Panel() {
setHasHydrated(true)
}, [setHasHydrated])
/**
* Focus Copilot user input when the Copilot tab becomes active or when
* the panel loads with Copilot already selected, after hydration.
*/
useEffect(() => {
if (!_hasHydrated || activeTab !== 'copilot') {
return
}
copilotRef.current?.focusInput()
}, [_hasHydrated, activeTab])
/**
* Handles tab click events
*/

View File

@@ -241,13 +241,13 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) {
{isLoading ? (
<div className='flex flex-col gap-[16px]'>
<div className='flex flex-col gap-[8px]'>
<Skeleton className='h-[14px] w-[70px]' />
<Skeleton className='h-5 w-[70px]' />
<div className='text-[13px] text-[var(--text-muted)]'>
<Skeleton className='h-[13px] w-[140px]' />
<Skeleton className='h-5 w-[140px]' />
</div>
</div>
<div className='flex flex-col gap-[8px]'>
<Skeleton className='h-[14px] w-[55px]' />
<Skeleton className='h-5 w-[55px]' />
<ApiKeySkeleton />
<ApiKeySkeleton />
</div>
@@ -624,10 +624,10 @@ function ApiKeySkeleton() {
<div className='flex items-center justify-between gap-[12px]'>
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
<div className='flex items-center gap-[6px]'>
<Skeleton className='h-[14px] w-[80px]' />
<Skeleton className='h-[13px] w-[140px]' />
<Skeleton className='h-5 w-[80px]' />
<Skeleton className='h-5 w-[140px]' />
</div>
<Skeleton className='h-[13px] w-[100px]' />
<Skeleton className='h-5 w-[100px]' />
</div>
<Skeleton className='h-[26px] w-[48px] rounded-[6px]' />
</div>

View File

@@ -752,13 +752,13 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
{isLoading ? (
<>
<div className='flex flex-col gap-[8px]'>
<Skeleton className='h-[14px] w-[70px]' />
<Skeleton className='h-5 w-[70px]' />
<div className='text-[13px] text-[var(--text-muted)]'>
<Skeleton className='h-[13px] w-[160px]' />
<Skeleton className='h-5 w-[160px]' />
</div>
</div>
<div className='flex flex-col gap-[8px]'>
<Skeleton className='h-[14px] w-[55px]' />
<Skeleton className='h-5 w-[55px]' />
{Array.from({ length: 2 }, (_, i) => (
<div key={`personal-${i}`} className={GRID_COLS}>
<Skeleton className='h-9 rounded-[6px]' />

View File

@@ -328,21 +328,28 @@ export function General({ onOpenChange }: GeneralProps) {
<div className='flex items-center gap-[8px]'>
{isEditingName ? (
<>
<input
ref={inputRef}
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleInputBlur}
className='w-auto border-0 bg-transparent p-0 font-medium text-[14px] outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
size={Math.max(name.length, 1)}
maxLength={100}
disabled={updateProfile.isPending}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
spellCheck='false'
/>
<div className='relative inline-flex'>
<span
className='invisible whitespace-pre font-medium text-[14px]'
aria-hidden='true'
>
{name || '\u00A0'}
</span>
<input
ref={inputRef}
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleInputBlur}
className='absolute top-0 left-0 h-full w-full border-0 bg-transparent p-0 font-medium text-[14px] outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
maxLength={100}
disabled={updateProfile.isPending}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
spellCheck='false'
/>
</div>
<Button
variant='ghost'
className='h-[12px] w-[12px] flex-shrink-0 p-0'
@@ -367,7 +374,7 @@ export function General({ onOpenChange }: GeneralProps) {
</>
)}
</div>
<p className='text-[13px] text-[var(--text-muted)]'>{profile?.email || ''}</p>
<p className='text-[13px] text-[var(--text-tertiary)]'>{profile?.email || ''}</p>
</div>
</div>
{uploadError && <p className='text-[13px] text-[var(--text-error)]'>{uploadError}</p>}
@@ -507,42 +514,34 @@ function GeneralSkeleton() {
<Skeleton className='h-9 w-9 rounded-full' />
<div className='flex flex-1 flex-col justify-center gap-[1px]'>
<div className='flex items-center gap-[8px]'>
<Skeleton className='h-[14px] w-24' />
<Skeleton className='h-[11px] w-[11px]' />
<Skeleton className='h-5 w-24' />
<Skeleton className='h-[10.5px] w-[10.5px]' />
</div>
<Skeleton className='h-[13px] w-40' />
<Skeleton className='h-5 w-40' />
</div>
</div>
{/* Theme row - temporarily hidden while light mode is disabled */}
{/* <div className='flex items-center justify-between border-b pb-[12px]'>
<Skeleton className='h-4 w-12' />
<Skeleton className='h-8 w-[100px] rounded-[8px]' />
</div> */}
{/* Auto-connect row */}
<div className='flex items-center justify-between'>
<div className='flex items-center justify-between pt-[12px]'>
<Skeleton className='h-4 w-36' />
<Skeleton className='h-[17px] w-[30px] rounded-[20px]' />
<Skeleton className='h-[17px] w-[30px] rounded-full' />
</div>
{/* Error notifications row */}
<div className='flex items-center justify-between'>
<Skeleton className='h-4 w-36' />
<Skeleton className='h-[17px] w-[30px] rounded-[20px]' />
<Skeleton className='h-4 w-40' />
<Skeleton className='h-[17px] w-[30px] rounded-full' />
</div>
{/* Telemetry row */}
<div className='flex items-center justify-between border-t pt-[12px]'>
<Skeleton className='h-4 w-40' />
<Skeleton className='h-[17px] w-[30px] rounded-[20px]' />
<Skeleton className='h-4 w-44' />
<Skeleton className='h-[17px] w-[30px] rounded-full' />
</div>
{/* Telemetry description */}
<div className='space-y-1'>
<Skeleton className='h-3 w-full' />
<Skeleton className='h-3 w-4/5' />
</div>
<Skeleton className='h-[12px] w-full' />
<Skeleton className='-mt-2 h-[12px] w-4/5' />
{/* Action buttons */}
<div className='mt-auto flex items-center gap-[8px]'>

View File

@@ -313,7 +313,7 @@ export function Integrations({ onOpenChange, registerCloseHandler }: Integration
<div className='flex flex-col gap-[16px]'>
{Object.entries(filteredGroupedServices).map(([providerKey, providerServices]) => (
<div key={providerKey} className='flex flex-col gap-[8px]'>
<Label className='text-[12px] text-[var(--text-muted)]'>
<Label className='text-[12px] text-[var(--text-tertiary)]'>
{OAUTH_PROVIDERS[providerKey]?.name || 'Other Services'}
</Label>
{providerServices.map((service) => (

View File

@@ -2,7 +2,7 @@ export { FormField } from './form-field/form-field'
export { FormattedInput } from './formatted-input/formatted-input'
export { HeaderRow } from './header-row/header-row'
export { McpServerSkeleton } from './mcp-server-skeleton/mcp-server-skeleton'
export { ServerListItem } from './server-list-item/server-list-item'
export { formatTransportLabel, ServerListItem } from './server-list-item/server-list-item'
export type {
EnvVarDropdownConfig,
HeaderEntry,

View File

@@ -14,7 +14,10 @@ export function McpServerSkeleton() {
</div>
<Skeleton className='h-[13px] w-[120px]' />
</div>
<Skeleton className='h-[30px] w-[54px] flex-shrink-0 rounded-[4px]' />
<div className='flex flex-shrink-0 items-center gap-[4px]'>
<Skeleton className='h-[30px] w-[60px] rounded-[4px]' />
<Skeleton className='h-[30px] w-[54px] rounded-[4px]' />
</div>
</div>
)
}

View File

@@ -3,7 +3,7 @@ import { Button } from '@/components/emcn'
/**
* Formats transport type for display (e.g., "streamable-http" -> "Streamable-HTTP").
*/
function formatTransportLabel(transport: string): string {
export function formatTransportLabel(transport: string): string {
return transport
.split('-')
.map((word) =>
@@ -29,9 +29,19 @@ interface ServerListItemProps {
tools: any[]
isDeleting: boolean
onRemove: () => void
onViewDetails: () => void
}
export function ServerListItem({ server, tools, isDeleting, onRemove }: ServerListItemProps) {
/**
* Renders a single MCP server list item with details and delete actions.
*/
export function ServerListItem({
server,
tools,
isDeleting,
onRemove,
onViewDetails,
}: ServerListItemProps) {
const transportLabel = formatTransportLabel(server.transport || 'http')
const toolsLabel = formatToolsLabel(tools)
@@ -46,9 +56,18 @@ export function ServerListItem({ server, tools, isDeleting, onRemove }: ServerLi
</div>
<p className='truncate text-[13px] text-[var(--text-muted)]'>{toolsLabel}</p>
</div>
<Button variant='ghost' className='flex-shrink-0' onClick={onRemove} disabled={isDeleting}>
{isDeleting ? 'Deleting...' : 'Delete'}
</Button>
<div className='flex flex-shrink-0 items-center gap-[4px]'>
<Button
variant='primary'
onClick={onViewDetails}
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
>
Details
</Button>
<Button variant='ghost' onClick={onRemove} disabled={isDeleting}>
{isDeleting ? 'Deleting...' : 'Delete'}
</Button>
</div>
</div>
)
}

View File

@@ -25,11 +25,25 @@ import type { InputFieldType, McpServerFormData, McpServerTestResult } from './c
import {
FormattedInput,
FormField,
formatTransportLabel,
HeaderRow,
McpServerSkeleton,
ServerListItem,
} from './components'
interface McpTool {
name: string
description?: string
serverId: string
}
interface McpServer {
id: string
name?: string
transport?: string
url?: string
}
const logger = createLogger('McpSettings')
const DEFAULT_FORM_DATA: McpServerFormData = {
@@ -86,6 +100,9 @@ export function MCP() {
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [serverToDelete, setServerToDelete] = useState<{ id: string; name: string } | null>(null)
// Server details view state
const [selectedServerId, setSelectedServerId] = useState<string | null>(null)
// Environment variable dropdown state
const [showEnvVars, setShowEnvVars] = useState(false)
const [envSearchTerm, setEnvSearchTerm] = useState('')
@@ -359,6 +376,31 @@ export function MCP() {
setShowAddForm(false)
}, [])
/**
* Opens the detail view for a specific server.
*/
const handleViewDetails = useCallback((serverId: string) => {
setSelectedServerId(serverId)
}, [])
/**
* Closes the detail view and returns to the server list.
*/
const handleBackToList = useCallback(() => {
setSelectedServerId(null)
}, [])
/**
* Gets the selected server and its tools for the detail view.
*/
const selectedServer = useMemo(() => {
if (!selectedServerId) return null
const server = servers.find((s) => s.id === selectedServerId) as McpServer | undefined
if (!server) return null
const serverTools = (toolsByServer[selectedServerId] || []) as McpTool[]
return { server, tools: serverTools }
}, [selectedServerId, servers, toolsByServer])
const error = toolsError || serversError
const hasServers = servers && servers.length > 0
const showEmptyState = !hasServers && !showAddForm
@@ -369,6 +411,80 @@ export function MCP() {
const isSubmitDisabled = serversLoading || isAddingServer || !isFormValid
const testButtonLabel = getTestButtonLabel(testResult, isTestingConnection)
// Show detail view if a server is selected
if (selectedServer) {
const { server, tools } = selectedServer
const transportLabel = formatTransportLabel(server.transport || 'http')
return (
<div className='flex h-full flex-col gap-[16px]'>
<div className='min-h-0 flex-1 overflow-y-auto'>
<div className='flex flex-col gap-[16px]'>
<div className='flex flex-col gap-[8px]'>
<span className='font-medium text-[13px] text-[var(--text-primary)]'>
Server Name
</span>
<p className='text-[14px] text-[var(--text-secondary)]'>
{server.name || 'Unnamed Server'}
</p>
</div>
<div className='flex flex-col gap-[8px]'>
<span className='font-medium text-[13px] text-[var(--text-primary)]'>Transport</span>
<p className='text-[14px] text-[var(--text-secondary)]'>{transportLabel}</p>
</div>
{server.url && (
<div className='flex flex-col gap-[8px]'>
<span className='font-medium text-[13px] text-[var(--text-primary)]'>URL</span>
<p className='break-all font-mono text-[13px] text-[var(--text-secondary)]'>
{server.url}
</p>
</div>
)}
<div className='flex flex-col gap-[8px]'>
<span className='font-medium text-[13px] text-[var(--text-primary)]'>
Tools ({tools.length})
</span>
{tools.length === 0 ? (
<p className='text-[13px] text-[var(--text-muted)]'>No tools available</p>
) : (
<div className='flex flex-col gap-[8px]'>
{tools.map((tool) => (
<div
key={tool.name}
className='rounded-[6px] border bg-[var(--surface-3)] px-[10px] py-[8px]'
>
<p className='font-medium text-[13px] text-[var(--text-primary)]'>
{tool.name}
</p>
{tool.description && (
<p className='mt-[4px] text-[13px] text-[var(--text-tertiary)]'>
{tool.description}
</p>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
<div className='mt-auto flex items-center justify-end'>
<Button
onClick={handleBackToList}
variant='primary'
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
>
Back
</Button>
</div>
</div>
)
}
return (
<>
<div className='flex h-full flex-col gap-[16px]'>
@@ -524,6 +640,7 @@ export function MCP() {
tools={tools}
isDeleting={deletingServers.has(server.id)}
onRemove={() => handleRemoveServer(server.id, server.name || 'this server')}
onViewDetails={() => handleViewDetails(server.id)}
/>
)
})}

View File

@@ -261,7 +261,7 @@ export function TemplateProfile() {
<div className='flex flex-col gap-[16px]'>
{/* Display Skeleton */}
<div className='flex flex-col gap-[8px]'>
<Skeleton className='h-[13px] w-[50px]' />
<Skeleton className='h-5 w-[50px]' />
<div className='flex items-center gap-[10px]'>
<Skeleton className='h-9 w-9 flex-shrink-0 rounded-full' />
<Skeleton className='h-9 flex-1' />
@@ -270,13 +270,13 @@ export function TemplateProfile() {
{/* About Skeleton */}
<div className='flex flex-col gap-[8px]'>
<Skeleton className='h-[13px] w-[35px]' />
<Skeleton className='h-5 w-[35px]' />
<Skeleton className='min-h-[100px] w-full' />
</div>
{/* Socials Skeleton */}
<div className='flex flex-col gap-[8px]'>
<Skeleton className='h-[13px] w-[45px]' />
<Skeleton className='h-5 w-[45px]' />
<Skeleton className='h-9 w-full' />
<Skeleton className='h-9 w-full' />
<Skeleton className='h-9 w-full' />

View File

@@ -48,6 +48,7 @@ import { ssoKeys, useSSOProviders } from '@/hooks/queries/sso'
import { subscriptionKeys, useSubscriptionData } from '@/hooks/queries/subscription'
const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED'))
const isSSOEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
interface SettingsModalProps {
open: boolean
@@ -167,6 +168,18 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
return false
}
// SSO has special logic that must be checked before requiresTeam
if (item.id === 'sso') {
if (isHosted) {
return hasOrganization && hasEnterprisePlan && canManageSSO
}
// For self-hosted, only show SSO tab if explicitly enabled via environment variable
if (!isSSOEnabled) return false
// Show tab if user is the SSO provider owner, or if no providers exist yet (to allow initial setup)
const hasProviders = (ssoProvidersData?.providers?.length ?? 0) > 0
return !hasProviders || isSSOProviderOwner === true
}
if (item.requiresTeam) {
const isMember = userRole === 'member' || isAdmin
const hasTeamPlan = subscriptionStatus.isTeam || subscriptionStatus.isEnterprise
@@ -185,13 +198,6 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
return false
}
if (item.id === 'sso') {
if (isHosted) {
return hasOrganization && hasEnterprisePlan && canManageSSO
}
return isSSOProviderOwner === true
}
if (item.requiresOwner && !isOwner) {
return false
}
@@ -203,6 +209,8 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
hasEnterprisePlan,
canManageSSO,
isSSOProviderOwner,
isSSOEnabled,
ssoProvidersData?.providers?.length,
isOwner,
isAdmin,
userRole,

View File

@@ -13,6 +13,7 @@ export { Input } from './input/input'
export { Label } from './label/label'
export {
Modal,
ModalBody,
ModalClose,
ModalContent,
type ModalContentProps,

View File

@@ -45,7 +45,7 @@ const Content = React.forwardRef<
collisionPadding={8}
avoidCollisions={true}
className={cn(
'z-50 rounded-[3px] bg-black px-[7.5px] py-[6px] font-base text-white text-xs shadow-md dark:bg-white dark:text-black',
'z-[10000300] rounded-[3px] bg-black px-[7.5px] py-[6px] font-base text-white text-xs shadow-md dark:bg-white dark:text-black',
className
)}
{...props}

View File

@@ -696,8 +696,8 @@ export function GrafanaIcon(props: SVGProps<SVGSVGElement>) {
y2='5.356'
gradientUnits='userSpaceOnUse'
>
<stop stop-color='#FFF200' />
<stop offset='1' stop-color='#F15A29' />
<stop stopColor='#FFF200' />
<stop offset='1' stopColor='#F15A29' />
</linearGradient>
</defs>
</svg>

View File

@@ -75,7 +75,7 @@ export function TagInput({
placeholder={value.length === 0 ? placeholder : ''}
disabled={disabled}
className={cn(
'h-6 min-w-[180px] flex-1 border-none bg-transparent p-0 text-[13px] focus-visible:ring-0 focus-visible:ring-offset-0',
'h-6 min-w-[180px] flex-1 border-none bg-transparent p-0 font-medium font-sans text-sm placeholder:text-[var(--text-muted)] focus-visible:ring-0 focus-visible:ring-offset-0',
value.length > 0 ? 'pl-[4px]' : 'pl-[4px]'
)}
/>

View File

@@ -50,10 +50,6 @@ export const usePanelEditorStore = create<PanelEditorState>()(
},
clearCurrentBlock: () => {
set({ currentBlockId: null })
// When selection is cleared (e.g. clicking on the canvas), switch to the toolbar tab
const panelState = usePanelStore.getState()
panelState.setActiveTab('toolbar')
},
setConnectionsHeight: (height) => {
const clampedHeight = Math.max(