first commit
This commit is contained in:
198
ui/lib/constants/config.ts
Normal file
198
ui/lib/constants/config.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { BaseProvider, ConcurrencyAndBufferSize, NetworkConfig } from "@/lib/types/config";
|
||||
import { ProviderName } from "./logs";
|
||||
|
||||
/**
|
||||
* Parse a date string in YYYY-MM-DD format with strict validation.
|
||||
* Returns null if the string is empty, malformed, or represents an invalid date.
|
||||
*/
|
||||
function parseTrialExpiry(dateStr: string | undefined): Date | null {
|
||||
if (!dateStr || !dateStr.trim()) return null;
|
||||
|
||||
// Strict format check: YYYY-MM-DD
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!dateRegex.test(dateStr)) return null;
|
||||
|
||||
const [year, month, day] = dateStr.split("-").map(Number);
|
||||
const date = new Date(year, month - 1, day);
|
||||
|
||||
// Validate the date components match (catches invalid dates like 2024-02-30)
|
||||
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
// Model placeholders based on provider type
|
||||
export const ModelPlaceholders = {
|
||||
default: "e.g. gpt-4, gpt-3.5-turbo. Leave blank for all models.",
|
||||
anthropic: "e.g. claude-3-haiku, claude-2.1",
|
||||
azure: "e.g. gpt-4, gpt-35-turbo (must match alias keys)",
|
||||
bedrock: "e.g. claude-v2, titan-text-express-v1, ai21-j2-mid",
|
||||
cerebras: "e.g. cerebras-2, cerebras-2-vision",
|
||||
cohere: "e.g. command-r, command-r-plus",
|
||||
gemini: "e.g. gemini-1.5-pro, gemini-1.5-flash",
|
||||
groq: "e.g. llama3-70b-8192, mixtral-8x7b-32768",
|
||||
huggingface: "e.g. sambanova/meta-llama/Llama-3.1-8B-Instruct, nebius/Qwen/Qwen3-Embedding-8B",
|
||||
mistral: "e.g. mistral-7b-instruct, mixtral-8x7b",
|
||||
openrouter: "e.g. openai/gpt-4, anthropic/claude-3-haiku",
|
||||
sgl: "e.g. sgl-2, sgl-vision",
|
||||
parasail: "e.g. parasail-2, parasail-vision",
|
||||
elevenlabs: "e.g. eleven_multilingual_v2, eleven_turbo_v2",
|
||||
perplexity: "e.g. sonar-pro, sonar-deep-research",
|
||||
ollama: "e.g. llama3.1, llama2",
|
||||
openai: "e.g. gpt-4, gpt-4o, gpt-4o-mini, gpt-3.5-turbo",
|
||||
vertex: "e.g. gemini-1.5-pro, text-bison, chat-bison",
|
||||
nebius: "e.g. openai/gpt-oss-120b, google/gemma-2-9b-it-fast, Qwen/Qwen2.5-VL-72B-Instruct",
|
||||
xai: "e.g. grok-4-0709, grok-3-mini, grok-3, grok-2-vision-1212",
|
||||
replicate: "e.g. meta/llama3-1-8b-instruct, black-forest-labs/flux-dev",
|
||||
vllm: "e.g. Qwen/Qwen3-0.6B, Qwen/Qwen3-1.5B",
|
||||
runway: "e.g. gen4_turbo_image_to_video, gen3a_turbo_image_to_video",
|
||||
fireworks: "e.g. accounts/fireworks/models/deepseek-v3p2",
|
||||
};
|
||||
|
||||
export const isKeyRequiredByProvider: Record<ProviderName, boolean> = {
|
||||
anthropic: true,
|
||||
azure: true,
|
||||
bedrock: true,
|
||||
cerebras: true,
|
||||
cohere: true,
|
||||
gemini: true,
|
||||
groq: true,
|
||||
huggingface: true,
|
||||
mistral: true,
|
||||
openrouter: true,
|
||||
sgl: false,
|
||||
parasail: true,
|
||||
elevenlabs: true,
|
||||
ollama: false,
|
||||
openai: true,
|
||||
vertex: true,
|
||||
perplexity: true,
|
||||
nebius: true,
|
||||
xai: true,
|
||||
replicate: true,
|
||||
runway: true,
|
||||
vllm: false,
|
||||
fireworks: true,
|
||||
};
|
||||
|
||||
export const DefaultNetworkConfig = {
|
||||
base_url: "",
|
||||
default_request_timeout_in_seconds: 30,
|
||||
max_retries: 0,
|
||||
retry_backoff_initial: 1000,
|
||||
retry_backoff_max: 10000,
|
||||
insecure_skip_verify: false,
|
||||
ca_cert_pem: "",
|
||||
stream_idle_timeout_in_seconds: 60,
|
||||
max_conns_per_host: 5000,
|
||||
enforce_http2: false,
|
||||
} satisfies NetworkConfig;
|
||||
|
||||
export const DefaultPerformanceConfig = {
|
||||
concurrency: 1000,
|
||||
buffer_size: 5000,
|
||||
} satisfies ConcurrencyAndBufferSize;
|
||||
|
||||
export const MCP_STATUS_COLORS = {
|
||||
connected: "bg-green-100 text-green-800",
|
||||
error: "bg-red-100 text-red-800",
|
||||
disconnected: "bg-gray-100 text-gray-800",
|
||||
} as const;
|
||||
|
||||
// Mapping of what IS supported by each base provider
|
||||
export const PROVIDER_SUPPORTED_REQUESTS: Record<BaseProvider, string[]> = {
|
||||
openai: [
|
||||
"list_models",
|
||||
"text_completion",
|
||||
"text_completion_stream",
|
||||
"chat_completion",
|
||||
"chat_completion_stream",
|
||||
"responses",
|
||||
"responses_stream",
|
||||
"embedding",
|
||||
"speech",
|
||||
"speech_stream",
|
||||
"transcription",
|
||||
"transcription_stream",
|
||||
"image_generation",
|
||||
"image_generation_stream",
|
||||
"image_edit",
|
||||
"image_edit_stream",
|
||||
"image_variation",
|
||||
"count_tokens",
|
||||
"video_generation",
|
||||
"video_retrieve",
|
||||
"video_download",
|
||||
"video_delete",
|
||||
"video_list",
|
||||
"video_remix",
|
||||
],
|
||||
anthropic: ["list_models", "chat_completion", "chat_completion_stream", "responses", "responses_stream", "count_tokens"],
|
||||
gemini: [
|
||||
"list_models",
|
||||
"chat_completion",
|
||||
"chat_completion_stream",
|
||||
"responses",
|
||||
"responses_stream",
|
||||
"embedding",
|
||||
"transcription",
|
||||
"transcription_stream",
|
||||
"speech",
|
||||
"speech_stream",
|
||||
"image_generation",
|
||||
"image_edit",
|
||||
"count_tokens",
|
||||
"video_generation",
|
||||
"video_retrieve",
|
||||
"video_download",
|
||||
"video_delete",
|
||||
"video_list",
|
||||
"video_remix",
|
||||
],
|
||||
cohere: ["list_models", "chat_completion", "chat_completion_stream", "responses", "responses_stream", "embedding", "count_tokens"],
|
||||
bedrock: [
|
||||
"list_models",
|
||||
"text_completion",
|
||||
"chat_completion",
|
||||
"chat_completion_stream",
|
||||
"responses",
|
||||
"responses_stream",
|
||||
"embedding",
|
||||
"image_generation",
|
||||
"image_edit",
|
||||
"image_variation",
|
||||
],
|
||||
replicate: [
|
||||
"list_models",
|
||||
"text_completion",
|
||||
"chat_completion",
|
||||
"chat_completion_stream",
|
||||
"responses",
|
||||
"responses_stream",
|
||||
"image_generation",
|
||||
"image_generation_stream",
|
||||
"image_edit",
|
||||
"image_edit_stream",
|
||||
"video_generation",
|
||||
"video_retrieve",
|
||||
"video_download",
|
||||
"video_delete",
|
||||
"video_list",
|
||||
"video_remix",
|
||||
],
|
||||
fireworks: [
|
||||
"list_models",
|
||||
"text_completion",
|
||||
"text_completion_stream",
|
||||
"chat_completion",
|
||||
"chat_completion_stream",
|
||||
"responses",
|
||||
"responses_stream",
|
||||
"embedding",
|
||||
],
|
||||
};
|
||||
|
||||
export const IS_ENTERPRISE = process.env.BIFROST_IS_ENTERPRISE === "true";
|
||||
export const TRIAL_EXPIRY = parseTrialExpiry(process.env.BIFROST_ENTERPRISE_TRIAL_EXPIRY);
|
||||
37
ui/lib/constants/governance.ts
Normal file
37
ui/lib/constants/governance.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Governance-related constants
|
||||
|
||||
export const resetDurationOptions = [
|
||||
{ label: "Every Minute", value: "1m" },
|
||||
{ label: "Every 5 Minutes", value: "5m" },
|
||||
{ label: "Every 15 Minutes", value: "15m" },
|
||||
{ label: "Every 30 Minutes", value: "30m" },
|
||||
{ label: "Hourly", value: "1h" },
|
||||
{ label: "Every 6 Hours", value: "6h" },
|
||||
{ label: "Daily", value: "1d" },
|
||||
{ label: "Weekly", value: "1w" },
|
||||
{ label: "Monthly", value: "1M" },
|
||||
];
|
||||
|
||||
export const budgetDurationOptions = [
|
||||
{ label: "Hourly", value: "1h" },
|
||||
{ label: "Daily", value: "1d" },
|
||||
{ label: "Weekly", value: "1w" },
|
||||
{ label: "Monthly", value: "1M" },
|
||||
];
|
||||
|
||||
// Durations that support calendar-aligned resets (snap to day/week/month/year boundaries).
|
||||
// Must stay in sync with IsCalendarAlignableDuration in framework/configstore/tables/utils.go.
|
||||
export const supportsCalendarAlignment = (duration: string): boolean => duration.length > 0 && /[dwMY]$/.test(duration);
|
||||
|
||||
// Map of duration values to short labels for display
|
||||
export const resetDurationLabels: Record<string, string> = {
|
||||
"1m": "Every Minute",
|
||||
"5m": "Every 5 Minutes",
|
||||
"15m": "Every 15 Minutes",
|
||||
"30m": "Every 30 Minutes",
|
||||
"1h": "Hourly",
|
||||
"6h": "Every 6 Hours",
|
||||
"1d": "Daily",
|
||||
"1w": "Weekly",
|
||||
"1M": "Monthly",
|
||||
};
|
||||
783
ui/lib/constants/icons.tsx
Normal file
783
ui/lib/constants/icons.tsx
Normal file
File diff suppressed because one or more lines are too long
11
ui/lib/constants/logs.test.ts
Normal file
11
ui/lib/constants/logs.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { RequestTypeColors, RequestTypeLabels, RequestTypes } from "./logs";
|
||||
|
||||
describe("logs constants", () => {
|
||||
it("registers realtime turn as a known request type", () => {
|
||||
expect(RequestTypes).toContain("realtime.turn");
|
||||
expect(RequestTypeLabels["realtime.turn"]).toBe("Realtime Turn");
|
||||
expect(RequestTypeColors["realtime.turn"]).toBeTruthy();
|
||||
});
|
||||
});
|
||||
308
ui/lib/constants/logs.ts
Normal file
308
ui/lib/constants/logs.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
// Known provider names array - centralized definition
|
||||
export const KnownProvidersNames = [
|
||||
"anthropic",
|
||||
"azure",
|
||||
"bedrock",
|
||||
"cerebras",
|
||||
"cohere",
|
||||
"gemini",
|
||||
"groq",
|
||||
"huggingface",
|
||||
"mistral",
|
||||
"ollama",
|
||||
"openai",
|
||||
"openrouter",
|
||||
"parasail",
|
||||
"elevenlabs",
|
||||
"perplexity",
|
||||
"sgl",
|
||||
"vertex",
|
||||
"nebius",
|
||||
"xai",
|
||||
"replicate",
|
||||
"vllm",
|
||||
"runway",
|
||||
"fireworks",
|
||||
] as const;
|
||||
|
||||
// Local Provider type derived from KNOWN_PROVIDERS constant
|
||||
export type ProviderName = (typeof KnownProvidersNames)[number];
|
||||
|
||||
export const ProviderNames: readonly ProviderName[] = KnownProvidersNames;
|
||||
|
||||
export const Statuses = ["success", "error", "processing", "cancelled"] as const;
|
||||
|
||||
export const RequestTypes = [
|
||||
"list_models",
|
||||
"text_completion",
|
||||
"text_completion_stream",
|
||||
"chat_completion",
|
||||
"chat_completion_stream",
|
||||
"responses",
|
||||
"responses_stream",
|
||||
"embedding",
|
||||
"rerank",
|
||||
"speech",
|
||||
"speech_stream",
|
||||
"transcription",
|
||||
"transcription_stream",
|
||||
"image_generation",
|
||||
"image_generation_stream",
|
||||
"image_edit",
|
||||
"image_edit_stream",
|
||||
"image_variation",
|
||||
"ocr",
|
||||
"ocr_stream",
|
||||
"video_generation",
|
||||
"video_retrieve",
|
||||
"video_download",
|
||||
"video_delete",
|
||||
"video_list",
|
||||
"video_remix",
|
||||
"count_tokens",
|
||||
// Container operations
|
||||
"container_create",
|
||||
"container_list",
|
||||
"container_retrieve",
|
||||
"container_delete",
|
||||
// Container file operations
|
||||
"container_file_create",
|
||||
"container_file_list",
|
||||
"container_file_retrieve",
|
||||
"container_file_content",
|
||||
"container_file_delete",
|
||||
"passthrough",
|
||||
"passthrough_stream",
|
||||
// WebSocket/Realtime operations
|
||||
"websocket_responses",
|
||||
"realtime",
|
||||
"realtime.turn",
|
||||
] as const;
|
||||
|
||||
export const ProviderLabels: Record<ProviderName, string> = {
|
||||
openai: "OpenAI",
|
||||
anthropic: "Anthropic",
|
||||
azure: "Azure",
|
||||
bedrock: "AWS Bedrock",
|
||||
cohere: "Cohere",
|
||||
vertex: "Vertex AI",
|
||||
mistral: "Mistral AI",
|
||||
ollama: "Ollama",
|
||||
groq: "Groq",
|
||||
parasail: "Parasail",
|
||||
elevenlabs: "Elevenlabs",
|
||||
perplexity: "Perplexity",
|
||||
sgl: "SGLang",
|
||||
cerebras: "Cerebras",
|
||||
gemini: "Gemini",
|
||||
openrouter: "OpenRouter",
|
||||
huggingface: "HuggingFace",
|
||||
nebius: "Nebius Token Factory",
|
||||
xai: "xAI",
|
||||
replicate: "Replicate",
|
||||
vllm: "vLLM",
|
||||
runway: "Runway",
|
||||
fireworks: "Fireworks AI",
|
||||
} as const;
|
||||
|
||||
// Helper function to get provider label, supporting custom providers
|
||||
export const getProviderLabel = (provider: string): string => {
|
||||
// Use hasOwnProperty for safe lookup without checking prototype chain
|
||||
if (Object.prototype.hasOwnProperty.call(ProviderLabels, provider.toLowerCase().trim() as ProviderName)) {
|
||||
return ProviderLabels[provider.toLowerCase().trim() as ProviderName];
|
||||
}
|
||||
|
||||
// For custom providers, return the original provider name as is
|
||||
return provider;
|
||||
};
|
||||
|
||||
export const StatusColors = {
|
||||
success: "bg-green-100 text-green-800",
|
||||
error: "bg-red-100 text-red-800",
|
||||
processing: "bg-blue-100 text-blue-800",
|
||||
cancelled: "bg-gray-100 text-gray-800",
|
||||
} as const;
|
||||
|
||||
export const StatusBarColors = {
|
||||
success: "bg-green-500",
|
||||
error: "bg-red-500",
|
||||
processing: "bg-blue-500",
|
||||
cancelled: "bg-gray-400",
|
||||
} as const;
|
||||
|
||||
export const RequestTypeLabels = {
|
||||
"chat.completion": "Chat",
|
||||
response: "Responses",
|
||||
"response.completion.chunk": "Responses Stream",
|
||||
completion: "Completion",
|
||||
"text.completion": "Text",
|
||||
list: "List",
|
||||
"audio.speech": "Speech",
|
||||
"audio.transcription": "Transcription",
|
||||
"chat.completion.chunk": "Chat Stream",
|
||||
"audio.speech.chunk": "Speech Stream",
|
||||
"audio.transcription.chunk": "Transcription Stream",
|
||||
|
||||
// Request Types
|
||||
list_models: "List Models",
|
||||
text_completion: "Text",
|
||||
text_completion_stream: "Text Stream",
|
||||
chat_completion: "Chat",
|
||||
chat_completion_stream: "Chat Stream",
|
||||
responses: "Responses",
|
||||
responses_stream: "Responses Stream",
|
||||
|
||||
embedding: "Embedding",
|
||||
rerank: "Rerank",
|
||||
|
||||
speech: "Speech",
|
||||
speech_stream: "Speech Stream",
|
||||
|
||||
transcription: "Transcription",
|
||||
transcription_stream: "Transcription Stream",
|
||||
|
||||
image_generation: "Image Generation",
|
||||
image_generation_stream: "Image Generation Stream",
|
||||
image_edit: "Image Edit",
|
||||
image_edit_stream: "Image Edit Stream",
|
||||
image_variation: "Image Variation",
|
||||
ocr: "OCR",
|
||||
ocr_stream: "OCR Stream",
|
||||
video_generation: "Video Generation",
|
||||
video_retrieve: "Video Retrieve",
|
||||
video_download: "Video Download",
|
||||
video_delete: "Video Delete",
|
||||
video_list: "Video List",
|
||||
video_remix: "Video Remix",
|
||||
count_tokens: "Count Tokens",
|
||||
|
||||
batch_create: "Batch Create",
|
||||
batch_list: "Batch List",
|
||||
batch_retrieve: "Batch Retrieve",
|
||||
batch_cancel: "Batch Cancel",
|
||||
batch_delete: "Batch Delete",
|
||||
batch_results: "Batch Results",
|
||||
|
||||
file_upload: "File Upload",
|
||||
file_list: "File List",
|
||||
file_retrieve: "File Retrieve",
|
||||
file_delete: "File Delete",
|
||||
file_content: "File Content",
|
||||
|
||||
// Container operations
|
||||
container_create: "Container Create",
|
||||
container_list: "Container List",
|
||||
container_retrieve: "Container Retrieve",
|
||||
container_delete: "Container Delete",
|
||||
|
||||
// Container file operations
|
||||
container_file_create: "Container File Create",
|
||||
container_file_list: "Container File List",
|
||||
container_file_retrieve: "Container File Retrieve",
|
||||
container_file_content: "Container File Content",
|
||||
container_file_delete: "Container File Delete",
|
||||
|
||||
passthrough: "Passthrough",
|
||||
passthrough_stream: "Passthrough Stream",
|
||||
// WebSocket operations
|
||||
websocket_responses: "WebSocket Responses",
|
||||
realtime: "Realtime",
|
||||
"realtime.turn": "Realtime Turn",
|
||||
} as const;
|
||||
|
||||
export const RequestTypeColors = {
|
||||
"chat.completion": "bg-blue-100 text-blue-800",
|
||||
response: "bg-teal-100 text-teal-800",
|
||||
"response.completion.chunk": "bg-violet-100 text-violet-800",
|
||||
"text.completion": "bg-green-100 text-green-800",
|
||||
list: "bg-red-100 text-red-800",
|
||||
"audio.speech": "bg-purple-100 text-purple-800",
|
||||
"audio.transcription": "bg-orange-100 text-orange-800",
|
||||
"chat.completion.chunk": "bg-yellow-100 text-yellow-800",
|
||||
"audio.speech.chunk": "bg-pink-100 text-pink-800",
|
||||
"audio.transcription.chunk": "bg-lime-100 text-lime-800",
|
||||
completion: "bg-yellow-100 text-yellow-800",
|
||||
|
||||
// Request Types
|
||||
list_models: "bg-green-100 text-green-800",
|
||||
text_completion: "bg-green-100 text-green-800",
|
||||
text_completion_stream: "bg-amber-100 text-amber-800",
|
||||
|
||||
chat_completion: "bg-blue-100 text-blue-800",
|
||||
chat_completion_stream: "bg-yellow-100 text-yellow-800",
|
||||
|
||||
responses: "bg-teal-100 text-teal-800",
|
||||
responses_stream: "bg-violet-100 text-violet-800",
|
||||
|
||||
embedding: "bg-red-100 text-red-800",
|
||||
rerank: "bg-fuchsia-100 text-fuchsia-800",
|
||||
|
||||
speech: "bg-purple-100 text-purple-800",
|
||||
speech_stream: "bg-pink-100 text-pink-800",
|
||||
|
||||
transcription: "bg-orange-100 text-orange-800",
|
||||
transcription_stream: "bg-lime-100 text-lime-800",
|
||||
|
||||
image_generation: "bg-indigo-100 text-indigo-800",
|
||||
image_generation_stream: "bg-sky-100 text-sky-800",
|
||||
image_edit: "bg-emerald-100 text-emerald-800",
|
||||
image_edit_stream: "bg-teal-100 text-teal-800",
|
||||
image_variation: "bg-violet-100 text-violet-800",
|
||||
ocr: "bg-amber-100 text-amber-800",
|
||||
ocr_stream: "bg-yellow-100 text-yellow-800",
|
||||
video_generation: "bg-fuchsia-100 text-fuchsia-800",
|
||||
video_retrieve: "bg-blue-100 text-blue-800",
|
||||
video_download: "bg-purple-100 text-purple-800",
|
||||
video_delete: "bg-rose-100 text-rose-800",
|
||||
video_list: "bg-cyan-100 text-cyan-800",
|
||||
video_remix: "bg-pink-100 text-pink-800",
|
||||
count_tokens: "bg-cyan-100 text-cyan-800",
|
||||
|
||||
// Container operations
|
||||
container_create: "bg-emerald-100 text-emerald-800",
|
||||
container_list: "bg-teal-100 text-teal-800",
|
||||
container_retrieve: "bg-cyan-100 text-cyan-800",
|
||||
container_delete: "bg-rose-100 text-rose-800",
|
||||
|
||||
// Container file operations
|
||||
container_file_create: "bg-emerald-100 text-emerald-800",
|
||||
container_file_list: "bg-teal-100 text-teal-800",
|
||||
container_file_retrieve: "bg-cyan-100 text-cyan-800",
|
||||
container_file_content: "bg-sky-100 text-sky-800",
|
||||
container_file_delete: "bg-rose-100 text-rose-800",
|
||||
|
||||
passthrough: "bg-slate-100 text-slate-800",
|
||||
passthrough_stream: "bg-slate-200 text-slate-800",
|
||||
|
||||
batch_create: "bg-green-100 text-green-800",
|
||||
batch_list: "bg-blue-100 text-blue-800",
|
||||
batch_retrieve: "bg-red-100 text-red-800",
|
||||
batch_cancel: "bg-yellow-100 text-yellow-800",
|
||||
batch_delete: "bg-amber-100 text-amber-800",
|
||||
batch_results: "bg-purple-100 text-purple-800",
|
||||
|
||||
file_upload: "bg-pink-100 text-pink-800",
|
||||
file_list: "bg-lime-100 text-lime-800",
|
||||
file_retrieve: "bg-orange-100 text-orange-800",
|
||||
file_delete: "bg-red-100 text-red-800",
|
||||
file_content: "bg-blue-100 text-blue-800",
|
||||
|
||||
// WebSocket operations
|
||||
websocket_responses: "bg-teal-100 text-teal-800",
|
||||
realtime: "bg-indigo-100 text-indigo-800",
|
||||
"realtime.turn": "bg-cyan-100 text-cyan-800",
|
||||
} as const;
|
||||
|
||||
export const RoutingEngineUsedLabels = {
|
||||
"routing-rule": "Routing Rule",
|
||||
governance: "Governance",
|
||||
loadbalancing: "Loadbalancing",
|
||||
} as const;
|
||||
|
||||
export const RoutingEngineUsedColors = {
|
||||
"routing-rule": "bg-blue-100 text-blue-800",
|
||||
governance: "bg-green-100 text-green-800",
|
||||
loadbalancing: "bg-red-100 text-red-800",
|
||||
} as const;
|
||||
|
||||
export type Status = (typeof Statuses)[number];
|
||||
Reference in New Issue
Block a user