mirror of
https://github.com/mayanayza/netvisor.git
synced 2025-12-10 08:24:08 -06:00
made RichSelect component and fixed dns node selection
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
<script lang="ts">
|
||||
import { ChevronDown } from 'lucide-svelte';
|
||||
|
||||
export let label: string = '';
|
||||
export let selectedValue: string = '';
|
||||
export let options: RichSelectOption[] = [];
|
||||
export let placeholder: string = 'Select an option...';
|
||||
export let required: boolean = false;
|
||||
export let disabled: boolean = false;
|
||||
export let error: string | null = null;
|
||||
export let onSelect: (value: string) => void;
|
||||
|
||||
// Optional props for customizing how options are rendered
|
||||
export let getOptionIcon: ((option: RichSelectOption) => any) | null = null;
|
||||
export let getOptionIconColor: ((option: RichSelectOption) => string) | null = null;
|
||||
export let getOptionBadge: ((option: RichSelectOption) => string | null) | null = null;
|
||||
export let getOptionBadgeColor: ((option: RichSelectOption) => string) | null = null;
|
||||
export let getOptionTag: ((option: RichSelectOption) => RichSelectTag | null) | null = null;
|
||||
export let getOptionStatusText: ((option: RichSelectOption) => string | null) | null = null;
|
||||
export let showDescriptionInTrigger: boolean = false;
|
||||
|
||||
interface RichSelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
metadata?: any;
|
||||
}
|
||||
|
||||
interface RichSelectTag {
|
||||
text: string;
|
||||
textColor: string;
|
||||
bgColor: string;
|
||||
}
|
||||
|
||||
let isOpen = false;
|
||||
let dropdownElement: HTMLDivElement;
|
||||
|
||||
$: selectedOption = options.find(opt => opt.value === selectedValue);
|
||||
|
||||
function handleSelect(value: string) {
|
||||
const option = options.find(opt => opt.value === value);
|
||||
if (option && !option.disabled) {
|
||||
onSelect(value);
|
||||
isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownElement && !dropdownElement.contains(event.target as Node)) {
|
||||
isOpen = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:click={handleClickOutside} />
|
||||
|
||||
<div class="space-y-2" bind:this={dropdownElement}>
|
||||
<!-- Label -->
|
||||
{#if label}
|
||||
<div class="block text-sm font-medium text-gray-300">
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="text-red-400 ml-1">*</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Dropdown Container -->
|
||||
<div class="relative">
|
||||
<!-- Dropdown Trigger -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => !disabled && (isOpen = !isOpen)}
|
||||
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center justify-between
|
||||
{error ? 'border-red-500' : ''}
|
||||
{disabled ? 'opacity-50 cursor-not-allowed' : ''}"
|
||||
{disabled}
|
||||
>
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
{#if selectedOption}
|
||||
<!-- Icon -->
|
||||
{#if getOptionIcon}
|
||||
{@const icon = getOptionIcon(selectedOption)}
|
||||
{#if icon}
|
||||
<div class="w-6 h-6 rounded bg-gray-600 flex items-center justify-center flex-shrink-0">
|
||||
<svelte:component
|
||||
this={icon}
|
||||
class="w-3 h-3 {getOptionIconColor ? getOptionIconColor(selectedOption) : 'text-gray-300'}"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Label and description -->
|
||||
<div class="flex-1 min-w-0 text-left">
|
||||
<span class="block truncate">{selectedOption.label}</span>
|
||||
{#if showDescriptionInTrigger && selectedOption.description}
|
||||
<span class="block text-xs text-gray-400 truncate">{selectedOption.description}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Tag -->
|
||||
{#if getOptionTag}
|
||||
{@const tag = getOptionTag(selectedOption)}
|
||||
{#if tag}
|
||||
<span class="inline-block px-2 py-0.5 text-xs rounded flex-shrink-0 {tag.textColor} {tag.bgColor}">
|
||||
{tag.text}
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<span class="text-gray-400">{placeholder}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ChevronDown class="w-4 h-4 text-gray-400 transition-transform flex-shrink-0 {isOpen ? 'rotate-180' : ''}" />
|
||||
</button>
|
||||
|
||||
<!-- Description below trigger (optional) -->
|
||||
{#if selectedOption && selectedOption.description && !showDescriptionInTrigger}
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-400">
|
||||
{selectedOption.description}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<div class="flex items-center gap-2 text-red-400 text-sm mt-1">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
{#if isOpen && !disabled}
|
||||
<div class="absolute z-50 w-full bg-gray-700 border border-gray-600 rounded-md shadow-lg max-h-96 overflow-y-auto mt-1">
|
||||
{#each options as option}
|
||||
{@const tag = getOptionTag ? getOptionTag(option) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => handleSelect(option.value)}
|
||||
class="w-full px-3 py-3 text-left transition-colors border-b border-gray-600 last:border-b-0
|
||||
{option.disabled ? 'cursor-not-allowed opacity-50' : 'hover:bg-gray-600'}"
|
||||
disabled={option.disabled}
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<!-- Icon -->
|
||||
{#if getOptionIcon}
|
||||
{@const icon = getOptionIcon(option)}
|
||||
{#if icon}
|
||||
<div class="w-8 h-8 rounded-lg bg-gray-600 flex items-center justify-center mt-0.5 flex-shrink-0">
|
||||
<svelte:component
|
||||
this={icon}
|
||||
class="w-4 h-4 {getOptionIconColor ? getOptionIconColor(option) : 'text-gray-300'}"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<h4 class="font-medium text-white">{option.label}</h4>
|
||||
|
||||
<!-- Badge -->
|
||||
{#if getOptionBadge}
|
||||
{@const badge = getOptionBadge(option)}
|
||||
{#if badge}
|
||||
<span class="inline-block px-2 py-1 text-xs rounded
|
||||
{getOptionBadgeColor ? getOptionBadgeColor(option) : 'bg-gray-600 text-gray-300'}">
|
||||
{badge}
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Tag -->
|
||||
{#if tag}
|
||||
<span class="inline-block px-2 py-1 text-xs rounded {tag.textColor} {tag.bgColor}">
|
||||
{tag.text}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
{#if option.description}
|
||||
<p class="text-sm text-gray-400 mt-1 line-clamp-2">{option.description}</p>
|
||||
{/if}
|
||||
|
||||
<!-- Status Text -->
|
||||
{#if getOptionStatusText}
|
||||
{@const statusText = getOptionStatusText(option)}
|
||||
{#if statusText}
|
||||
<p class="text-xs mt-1 text-gray-400">
|
||||
{statusText}
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Circle } from "lucide-svelte";
|
||||
import type { Component } from "svelte";
|
||||
import { getBgColor, getTextColor } from "./colors";
|
||||
|
||||
export let icon = Circle;
|
||||
export let enableIcon = false
|
||||
|
||||
export let color: string = 'gray';
|
||||
export let bgColor: string = 'bg-gray-700/30';
|
||||
export let textColor: string = 'text-gray-500';
|
||||
|
||||
@@ -13,6 +15,11 @@
|
||||
|
||||
export let badge: string = ''
|
||||
|
||||
if (color !== 'gray') {
|
||||
bgColor = getBgColor(color);
|
||||
textColor = getTextColor(color);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="items-center space-x-2">
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { TestConfigSchema } from '$lib/components/tests/types';
|
||||
export let schema: TestConfigSchema;
|
||||
</script>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Compatibility Status -->
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg border
|
||||
{schema.compatibility === 'Compatible' ? 'bg-green-900/20 border-green-600' :
|
||||
schema.compatibility === 'Conditional' ? 'bg-yellow-900/20 border-yellow-600' :
|
||||
'bg-red-900/20 border-red-600'}">
|
||||
<iconify-icon
|
||||
icon="mdi:{schema.compatibility === 'Compatible' ? 'check-circle' :
|
||||
schema.compatibility === 'Conditional' ? 'alert' :
|
||||
'close-circle'}"
|
||||
class="{schema.compatibility === 'Compatible' ? 'text-green-400' :
|
||||
schema.compatibility === 'Conditional' ? 'text-yellow-400' :
|
||||
'text-red-400'}"
|
||||
></iconify-icon>
|
||||
|
||||
<div class="flex-1">
|
||||
<span class="text-sm font-medium
|
||||
{schema.compatibility === 'Compatible' ? 'text-green-200' :
|
||||
schema.compatibility === 'Conditional' ? 'text-yellow-200' :
|
||||
'text-red-200'}">
|
||||
{#if schema.compatibility === 'Compatible'}
|
||||
Compatible
|
||||
{:else if schema.compatibility === 'Conditional'}
|
||||
Conditionally Compatible
|
||||
{:else}
|
||||
Incompatible
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if schema.compatibility_reason}
|
||||
<p class="text-xs mt-1
|
||||
{schema.compatibility === 'Compatible' ? 'text-green-300' :
|
||||
schema.compatibility === 'Conditional' ? 'text-yellow-300' :
|
||||
'text-red-300'}">
|
||||
{schema.compatibility_reason}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Messages -->
|
||||
{#if schema.errors.length > 0}
|
||||
<div class="space-y-2">
|
||||
{#each schema.errors as error}
|
||||
<div class="flex items-start gap-2 p-3 bg-red-900/20 border border-red-600 rounded-lg">
|
||||
<iconify-icon icon="mdi:alert-circle" class="text-red-400 mt-0.5"></iconify-icon>
|
||||
<div class="flex-1">
|
||||
<span class="text-sm text-red-200">{error.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,18 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { AlertCircle, Server } from 'lucide-svelte';
|
||||
import type { ConfigField } from '$lib/components/tests/types';
|
||||
import type { ConfigField } from '$lib/components/tests/types'
|
||||
import RichSelect from '../../../common/RichSelect.svelte';
|
||||
|
||||
export let field: ConfigField;
|
||||
export let value: any;
|
||||
export let error: string | null = null;
|
||||
export let onUpdate: (value: any) => void;
|
||||
|
||||
// Stable value to prevent unnecessary re-renders
|
||||
let inputValue = value || '';
|
||||
// Use default value if no value is provided
|
||||
function getInitialValue() {
|
||||
// If a value is explicitly provided, use it
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Otherwise, use the field's default value
|
||||
if (field.default_value !== undefined && field.default_value !== null) {
|
||||
return field.default_value;
|
||||
}
|
||||
|
||||
// Fallback to empty string or appropriate default based on field type
|
||||
switch (field.field_type.base_type) {
|
||||
case 'boolean':
|
||||
return false;
|
||||
case 'integer':
|
||||
return '';
|
||||
case 'select':
|
||||
case 'node_selector':
|
||||
return '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Update internal value when prop changes
|
||||
$: if (value !== inputValue) {
|
||||
inputValue = value || '';
|
||||
// Initialize with the determined initial value
|
||||
let inputValue = getInitialValue();
|
||||
|
||||
// Update internal value when prop changes, but preserve defaults
|
||||
$: {
|
||||
const newInitialValue = getInitialValue();
|
||||
if (newInitialValue !== inputValue) {
|
||||
inputValue = newInitialValue;
|
||||
// Notify parent of the default value if it wasn't already set
|
||||
if ((value === undefined || value === null || value === '') &&
|
||||
field.default_value !== undefined && field.default_value !== null) {
|
||||
onUpdate(field.default_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleInput(event: Event) {
|
||||
@@ -32,6 +67,14 @@
|
||||
// Call parent update
|
||||
onUpdate(newValue);
|
||||
}
|
||||
|
||||
function handleRichSelectChange(value: any) {
|
||||
// Update internal value immediately
|
||||
inputValue = value;
|
||||
|
||||
// Call parent update
|
||||
onUpdate(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#key field.id}
|
||||
@@ -101,59 +144,34 @@
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
{:else if field.field_type.base_type === 'node_selector'}
|
||||
<div class="space-y-3">
|
||||
{#if !field.field_type.options || field.field_type.options.length === 0}
|
||||
<div class="p-4 bg-yellow-900/20 border border-yellow-600 rounded-lg">
|
||||
<div class="flex items-center gap-2">
|
||||
<AlertCircle class="w-4 h-4 text-yellow-400" />
|
||||
<span class="text-sm text-yellow-200">No compatible nodes available</span>
|
||||
</div>
|
||||
<p class="text-xs text-yellow-300 mt-1">
|
||||
Create a node with the required capabilities first.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
{#each field.field_type.options as nodeOption}
|
||||
<label class="flex items-start gap-3 p-3 bg-gray-700/50 border border-gray-600 rounded-lg
|
||||
cursor-pointer hover:bg-gray-700/70 transition-colors
|
||||
{inputValue === nodeOption.value ? 'border-blue-500 bg-blue-900/20' : ''}">
|
||||
<input
|
||||
type="radio"
|
||||
name={field.id}
|
||||
value={nodeOption.value}
|
||||
checked={inputValue === nodeOption.value}
|
||||
on:change={handleInput}
|
||||
class="mt-0.5 text-blue-600 bg-gray-700 border-gray-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<Server class="w-4 h-4 text-blue-400" />
|
||||
<span class="font-medium text-white">{nodeOption.label}</span>
|
||||
</div>
|
||||
{#if nodeOption.description}
|
||||
<p class="text-sm text-gray-400 mt-1">{nodeOption.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if field.field_type.base_type === 'rich_select'}
|
||||
<RichSelect
|
||||
selectedValue={inputValue}
|
||||
options={field.field_type.options?.map(opt => ({
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
description: opt.description,
|
||||
disabled: opt.disabled || false
|
||||
})) || []}
|
||||
placeholder={field.placeholder || 'Select an option...'}
|
||||
required={field.required}
|
||||
error={error}
|
||||
onSelect={handleRichSelectChange}
|
||||
getOptionIcon={field.id.includes('node') ? () => Server : null}
|
||||
getOptionIconColor={field.id.includes('node') ? () => 'text-blue-400' : null}
|
||||
/>
|
||||
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="flex items-center gap-2 text-red-400 text-sm">
|
||||
<AlertCircle class="w-4 h-4" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Help Text -->
|
||||
{#if field.help_text && field.field_type.base_type !== 'boolean'}
|
||||
<p class="text-xs text-gray-400">{field.help_text}</p>
|
||||
{/if}
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<p class="text-xs text-red-400 flex items-center gap-1">
|
||||
<AlertCircle class="w-3 h-3" />
|
||||
{error}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
@@ -14,8 +14,8 @@
|
||||
import { type AssignedTest, type NodeFormData } from '$lib/components/nodes/types';
|
||||
import { criticalityLevels } from '$lib/api/registry';
|
||||
import DynamicField from './DynamicField.svelte';
|
||||
import CompatibilityIndicator from './CompatibilityIndicator.svelte';
|
||||
import TestTypeDropdown from './TestTypeDropdown.svelte';
|
||||
import Tag from '$lib/components/common/Tag.svelte';
|
||||
|
||||
export let test: AssignedTest | null = null;
|
||||
export let onClose: () => void;
|
||||
@@ -235,12 +235,6 @@
|
||||
</div>
|
||||
|
||||
{:else if schema}
|
||||
<!-- Incompatible Test -->
|
||||
{#if schema.compatibility !== 'Compatible'}
|
||||
<CompatibilityIndicator
|
||||
schema={schema}
|
||||
/>
|
||||
{/if}
|
||||
{#if schema.compatibility === 'Compatible'}
|
||||
<div class="bg-gray-800/50 rounded-lg p-4 border border-gray-700">
|
||||
<label for="test-criticality" class="block text-sm font-medium text-gray-200 mb-2">
|
||||
|
||||
@@ -1,137 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { ChevronDown, Server, CheckCircle, AlertTriangle, XCircle } from 'lucide-svelte';
|
||||
import { Server } from 'lucide-svelte';
|
||||
import { testTypes } from '$lib/api/registry';
|
||||
import RichSelect from '../../../common/RichSelect.svelte'
|
||||
|
||||
export let selectedTestType: string;
|
||||
export let onTestTypeChange: (testType: string) => void;
|
||||
export let schemaCache: Map<string, any> = new Map(); // Schema cache from parent
|
||||
|
||||
let isOpen = false;
|
||||
let dropdownElement: HTMLDivElement;
|
||||
|
||||
let types = $testTypes.sort((a,b) => schemaCache.get(a.id).compatibility == 'Compatible' ? -1 : 1)
|
||||
$: types = $testTypes.sort((a, b) => {
|
||||
const aSchema = schemaCache.get(a.id);
|
||||
const bSchema = schemaCache.get(b.id);
|
||||
if (aSchema?.compatibility === 'Compatible' && bSchema?.compatibility !== 'Compatible') return -1;
|
||||
if (bSchema?.compatibility === 'Compatible' && aSchema?.compatibility !== 'Compatible') return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
$: selectedTestInfo = types.find(t => t.id === selectedTestType);
|
||||
|
||||
function handleSelect(testType: string) {
|
||||
onTestTypeChange(testType);
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownElement && !dropdownElement.contains(event.target as Node)) {
|
||||
isOpen = false;
|
||||
// Transform test types into RichSelectOption format
|
||||
$: richOptions = types.map(testType => ({
|
||||
value: testType.id,
|
||||
label: testType.display_name,
|
||||
description: testType.description,
|
||||
disabled: false,
|
||||
metadata: {
|
||||
category: testType.category,
|
||||
color: testType.color,
|
||||
schema: schemaCache.get(testType.id)
|
||||
}
|
||||
}));
|
||||
|
||||
function getOptionIcon(option: any) {
|
||||
return Server;
|
||||
}
|
||||
|
||||
function getCompatibilityInfo(testTypeId: string) {
|
||||
const schema = schemaCache.get(testTypeId);
|
||||
function getOptionIconColor(option: any) {
|
||||
return option.metadata?.color || 'text-gray-300';
|
||||
}
|
||||
|
||||
function getOptionBadge(option: any) {
|
||||
return option.metadata?.category || null;
|
||||
}
|
||||
|
||||
function getOptionBadgeColor(option: any) {
|
||||
return 'bg-gray-600 text-gray-300';
|
||||
}
|
||||
|
||||
function getOptionTag(option: any) {
|
||||
const schema = option.metadata?.schema;
|
||||
if (!schema) return null;
|
||||
|
||||
return {
|
||||
status: schema.compatibility,
|
||||
reason: schema.compatibility_reason
|
||||
};
|
||||
}
|
||||
|
||||
function getCompatibilityColor(status: string) {
|
||||
const status = schema.compatibility;
|
||||
switch (status) {
|
||||
case 'Compatible':
|
||||
return 'text-green-400';
|
||||
return {
|
||||
text: status,
|
||||
textColor: 'text-green-300',
|
||||
bgColor: 'bg-green-900/30'
|
||||
};
|
||||
case 'Conditional':
|
||||
return 'text-yellow-400';
|
||||
return {
|
||||
text: status,
|
||||
textColor: 'text-yellow-300',
|
||||
bgColor: 'bg-yellow-900/30'
|
||||
};
|
||||
case 'Incompatible':
|
||||
return 'text-red-400';
|
||||
return {
|
||||
text: status,
|
||||
textColor: 'text-red-300',
|
||||
bgColor: 'bg-red-900/30'
|
||||
};
|
||||
default:
|
||||
return 'text-gray-400';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getOptionStatusText(option: any) {
|
||||
const schema = option.metadata?.schema;
|
||||
return schema?.compatibility_reason || null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:click={handleClickOutside} />
|
||||
|
||||
<div class="relative" bind:this={dropdownElement}>
|
||||
<div class="block text-sm font-medium text-gray-300 mb-2">
|
||||
Test Type
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Trigger -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => isOpen = !isOpen}
|
||||
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center justify-between"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
{#if selectedTestInfo}
|
||||
<div class="w-6 h-6 rounded bg-gray-600 flex items-center justify-center">
|
||||
<Server class="w-3 h-3 {selectedTestInfo.color}" />
|
||||
</div>
|
||||
<span>{selectedTestInfo.display_name}</span>
|
||||
{:else}
|
||||
<span class="text-gray-400">Select a test type...</span>
|
||||
{/if}
|
||||
</div>
|
||||
<ChevronDown class="w-4 h-4 text-gray-400 transition-transform {isOpen ? 'rotate-180' : ''}" />
|
||||
</button>
|
||||
|
||||
<!-- Test Description - inline text below dropdown -->
|
||||
{#if selectedTestInfo}
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-400">
|
||||
{selectedTestInfo.description}
|
||||
<span class="ml-2 px-2 py-0.5 text-xs bg-gray-600 text-gray-300 rounded">
|
||||
{selectedTestInfo.category}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
{#if isOpen}
|
||||
<div class="absolute z-50 w-full bg-gray-700 border border-gray-600 rounded-md shadow-lg max-h-96 overflow-y-auto" style="top: calc(100% + 0.5rem);">
|
||||
{#each $testTypes as testType}
|
||||
{@const compatibilityInfo = getCompatibilityInfo(testType.id)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
on:click={(compatibilityInfo?.status != 'Incompatible' ? () => handleSelect(testType.id) : () => {})}
|
||||
class="w-full px-3 py-3 text-left transition-colors border-b border-gray-600 last:border-b-0
|
||||
{compatibilityInfo?.status === 'Incompatible' ? 'cursor-default' : 'hover:bg-gray-600'}"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-gray-600 flex items-center justify-center mt-0.5">
|
||||
<Server class="w-4 h-4 {testType.color}" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<h4 class="font-medium text-white">{testType.display_name}</h4>
|
||||
|
||||
<span class="inline-block px-2 py-1 text-xs bg-gray-600 text-gray-300 rounded">
|
||||
{testType.category}
|
||||
</span>
|
||||
|
||||
{#if compatibilityInfo}
|
||||
<span class="inline-block px-2 py-1 text-xs rounded
|
||||
{compatibilityInfo.status === 'Compatible' ? 'bg-green-900/30 text-green-300' :
|
||||
compatibilityInfo.status === 'Conditional' ? 'bg-yellow-900/30 text-yellow-300' :
|
||||
'bg-red-900/30 text-red-300'}">
|
||||
{compatibilityInfo.status}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-400 mt-1 line-clamp-2">{testType.description}</p>
|
||||
|
||||
{#if compatibilityInfo?.reason}
|
||||
<p class="text-xs mt-1 {getCompatibilityColor(compatibilityInfo.status)}">
|
||||
{compatibilityInfo.reason}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<RichSelect
|
||||
label="Test Type"
|
||||
selectedValue={selectedTestType}
|
||||
options={richOptions}
|
||||
placeholder="Select a test type..."
|
||||
required={true}
|
||||
showDescriptionInTrigger={false}
|
||||
onSelect={onTestTypeChange}
|
||||
{getOptionIcon}
|
||||
{getOptionIconColor}
|
||||
{getOptionBadge}
|
||||
{getOptionBadgeColor}
|
||||
{getOptionTag}
|
||||
{getOptionStatusText}
|
||||
/>
|
||||
Reference in New Issue
Block a user