mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-05 13:20:57 +00:00
Initial commit
This commit is contained in:
46
lib/utils/icons.ts
Normal file
46
lib/utils/icons.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
Server, Globe, Cloud, Database, HardDrive, Cpu, Network, Box, Container, Monitor,
|
||||
Laptop, Smartphone, Tablet, Tv, Router, Wifi, Cable, Radio, Satellite, Building,
|
||||
Building2, Factory, Warehouse, House, Castle, Landmark, Store, School, Hospital,
|
||||
Terminal, Code, Binary, Braces, FileCode, GitBranch, GitCommitHorizontal, GitPullRequest,
|
||||
Settings, Cog, Wrench, Hammer, Package, Archive, FolderOpen,
|
||||
Shield, ShieldCheck, Lock, Key, Eye, EyeOff, TriangleAlert,
|
||||
Zap, Flame, Snowflake, Sun, Moon, Star, Sparkles, Heart, Crown, Gem,
|
||||
Anchor, Ship, Plane, Rocket, Car, Bike, TrainFront, Bus, Truck,
|
||||
Activity, BarChart3, ChartLine, ChartPie, TrendingUp, Gauge, Timer,
|
||||
Mail, MessageSquare, Phone, Video, Camera, Music, Headphones, Volume2,
|
||||
MapPin, Map, Compass, Navigation, Flag, Bookmark, Target
|
||||
} from 'lucide-svelte';
|
||||
import type { ComponentType } from 'svelte';
|
||||
|
||||
// Icon mapping for rendering
|
||||
const iconMap: Record<string, ComponentType> = {
|
||||
'server': Server, 'globe': Globe, 'cloud': Cloud, 'database': Database, 'hard-drive': HardDrive,
|
||||
'cpu': Cpu, 'network': Network, 'box': Box, 'container': Container, 'monitor': Monitor,
|
||||
'laptop': Laptop, 'smartphone': Smartphone, 'tablet': Tablet, 'tv': Tv, 'router': Router,
|
||||
'wifi': Wifi, 'cable': Cable, 'radio': Radio, 'satellite': Satellite, 'building': Building,
|
||||
'building-2': Building2, 'factory': Factory, 'warehouse': Warehouse, 'home': House, 'castle': Castle,
|
||||
'landmark': Landmark, 'store': Store, 'school': School, 'hospital': Hospital,
|
||||
'terminal': Terminal, 'code': Code, 'binary': Binary, 'braces': Braces, 'file-code': FileCode,
|
||||
'git-branch': GitBranch, 'git-commit': GitCommitHorizontal, 'git-pull-request': GitPullRequest,
|
||||
'settings': Settings, 'cog': Cog, 'wrench': Wrench, 'hammer': Hammer,
|
||||
'package': Package, 'archive': Archive, 'folder-open': FolderOpen,
|
||||
'shield': Shield, 'shield-check': ShieldCheck, 'lock': Lock, 'key': Key,
|
||||
'eye': Eye, 'eye-off': EyeOff, 'alert-triangle': TriangleAlert,
|
||||
'zap': Zap, 'flame': Flame, 'snowflake': Snowflake, 'sun': Sun, 'moon': Moon,
|
||||
'star': Star, 'sparkles': Sparkles, 'heart': Heart, 'crown': Crown, 'gem': Gem,
|
||||
'anchor': Anchor, 'ship': Ship, 'plane': Plane, 'rocket': Rocket, 'car': Car,
|
||||
'bike': Bike, 'train': TrainFront, 'bus': Bus, 'truck': Truck,
|
||||
'activity': Activity, 'bar-chart': BarChart3, 'line-chart': ChartLine, 'pie-chart': ChartPie,
|
||||
'trending-up': TrendingUp, 'gauge': Gauge, 'timer': Timer,
|
||||
'mail': Mail, 'message-square': MessageSquare, 'phone': Phone, 'video': Video,
|
||||
'camera': Camera, 'music': Music, 'headphones': Headphones, 'volume-2': Volume2,
|
||||
'map-pin': MapPin, 'map': Map, 'compass': Compass, 'navigation': Navigation,
|
||||
'flag': Flag, 'bookmark': Bookmark, 'target': Target
|
||||
};
|
||||
|
||||
export function getIconComponent(iconName: string): ComponentType {
|
||||
return iconMap[iconName] || Globe;
|
||||
}
|
||||
|
||||
export { iconMap };
|
||||
15
lib/utils/ip.ts
Normal file
15
lib/utils/ip.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Convert IP address (with optional CIDR) to numeric value for sorting
|
||||
* e.g., "192.168.1.0/24" -> 3232235776, "10.0.0.1" -> 167772161
|
||||
*/
|
||||
export function ipToNumber(ip: string | undefined | null): number {
|
||||
if (!ip || ip === '-') return Infinity; // Push empty IPs to the end
|
||||
// Strip CIDR notation if present
|
||||
const ipOnly = ip.split('/')[0];
|
||||
const parts = ipOnly.split('.');
|
||||
if (parts.length !== 4) return Infinity;
|
||||
return parts.reduce((acc, octet) => {
|
||||
const num = parseInt(octet, 10);
|
||||
return isNaN(num) ? Infinity : (acc << 8) + num;
|
||||
}, 0) >>> 0; // Convert to unsigned 32-bit
|
||||
}
|
||||
107
lib/utils/label-colors.ts
Normal file
107
lib/utils/label-colors.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Label color utilities for environment labels
|
||||
*
|
||||
* Provides consistent, deterministic color assignment based on label string hash.
|
||||
* Colors are from Tailwind's color palette for visual consistency.
|
||||
*/
|
||||
|
||||
// Tailwind color palette - vibrant, distinguishable colors
|
||||
const LABEL_COLORS = [
|
||||
'#ef4444', // red-500
|
||||
'#f97316', // orange-500
|
||||
'#eab308', // yellow-500
|
||||
'#22c55e', // green-500
|
||||
'#14b8a6', // teal-500
|
||||
'#3b82f6', // blue-500
|
||||
'#8b5cf6', // violet-500
|
||||
'#ec4899', // pink-500
|
||||
'#06b6d4', // cyan-500
|
||||
'#84cc16', // lime-500
|
||||
'#6366f1', // indigo-500
|
||||
'#d946ef' // fuchsia-500
|
||||
];
|
||||
|
||||
// Lighter variants for backgrounds (with alpha)
|
||||
const LABEL_BG_COLORS = [
|
||||
'rgba(239, 68, 68, 0.15)', // red
|
||||
'rgba(249, 115, 22, 0.15)', // orange
|
||||
'rgba(234, 179, 8, 0.15)', // yellow
|
||||
'rgba(34, 197, 94, 0.15)', // green
|
||||
'rgba(20, 184, 166, 0.15)', // teal
|
||||
'rgba(59, 130, 246, 0.15)', // blue
|
||||
'rgba(139, 92, 246, 0.15)', // violet
|
||||
'rgba(236, 72, 153, 0.15)', // pink
|
||||
'rgba(6, 182, 212, 0.15)', // cyan
|
||||
'rgba(132, 204, 22, 0.15)', // lime
|
||||
'rgba(99, 102, 241, 0.15)', // indigo
|
||||
'rgba(217, 70, 239, 0.15)' // fuchsia
|
||||
];
|
||||
|
||||
/**
|
||||
* Generate a hash from a string for consistent color assignment
|
||||
*/
|
||||
function hashString(str: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return Math.abs(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary color for a label (for text/borders)
|
||||
*/
|
||||
export function getLabelColor(label: string): string {
|
||||
const index = hashString(label) % LABEL_COLORS.length;
|
||||
return LABEL_COLORS[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the background color for a label (lighter, with transparency)
|
||||
*/
|
||||
export function getLabelBgColor(label: string): string {
|
||||
const index = hashString(label) % LABEL_BG_COLORS.length;
|
||||
return LABEL_BG_COLORS[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get both colors for a label as an object
|
||||
*/
|
||||
export function getLabelColors(label: string): { color: string; bgColor: string } {
|
||||
const index = hashString(label) % LABEL_COLORS.length;
|
||||
return {
|
||||
color: LABEL_COLORS[index],
|
||||
bgColor: LABEL_BG_COLORS[index]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum number of labels allowed per environment
|
||||
*/
|
||||
export const MAX_LABELS = 10;
|
||||
|
||||
/**
|
||||
* Parse labels from JSON string or array (handles both database and API formats)
|
||||
*/
|
||||
export function parseLabels(labels: string | string[] | null | undefined): string[] {
|
||||
if (!labels) return [];
|
||||
// Already an array - return as-is
|
||||
if (Array.isArray(labels)) return labels;
|
||||
// JSON string from database
|
||||
try {
|
||||
const parsed = JSON.parse(labels);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize labels to JSON string for database storage
|
||||
*/
|
||||
export function serializeLabels(labels: string[]): string | null {
|
||||
if (!labels || labels.length === 0) return null;
|
||||
return JSON.stringify(labels);
|
||||
}
|
||||
160
lib/utils/update-steps.ts
Normal file
160
lib/utils/update-steps.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Shared utilities for update step visualization across:
|
||||
* - BatchUpdateModal (manual update from containers grid)
|
||||
* - Scheduled container auto-updates
|
||||
* - Scheduled environment update checks
|
||||
*/
|
||||
|
||||
import {
|
||||
Download,
|
||||
Shield,
|
||||
Square,
|
||||
Trash2,
|
||||
Box,
|
||||
Play,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
ShieldBan,
|
||||
Circle,
|
||||
Loader2,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
ShieldOff,
|
||||
ShieldX
|
||||
} from 'lucide-svelte';
|
||||
import type { ComponentType } from 'svelte';
|
||||
import type { VulnerabilityCriteria } from '$lib/server/db';
|
||||
|
||||
// Step types for update process
|
||||
export type StepType =
|
||||
| 'pulling'
|
||||
| 'scanning'
|
||||
| 'stopping'
|
||||
| 'removing'
|
||||
| 'creating'
|
||||
| 'starting'
|
||||
| 'done'
|
||||
| 'failed'
|
||||
| 'blocked'
|
||||
| 'checked'
|
||||
| 'skipped'
|
||||
| 'updated';
|
||||
|
||||
// Get icon component for a step
|
||||
export function getStepIcon(step: StepType): ComponentType {
|
||||
switch (step) {
|
||||
case 'pulling':
|
||||
return Download;
|
||||
case 'scanning':
|
||||
return Shield;
|
||||
case 'stopping':
|
||||
return Square;
|
||||
case 'removing':
|
||||
return Trash2;
|
||||
case 'creating':
|
||||
return Box;
|
||||
case 'starting':
|
||||
return Play;
|
||||
case 'done':
|
||||
case 'updated':
|
||||
return CheckCircle2;
|
||||
case 'failed':
|
||||
return XCircle;
|
||||
case 'blocked':
|
||||
return ShieldBan;
|
||||
case 'checked':
|
||||
case 'skipped':
|
||||
return Circle;
|
||||
default:
|
||||
return Loader2;
|
||||
}
|
||||
}
|
||||
|
||||
// Get human-readable label for a step
|
||||
export function getStepLabel(step: StepType): string {
|
||||
switch (step) {
|
||||
case 'pulling':
|
||||
return 'Pulling image';
|
||||
case 'scanning':
|
||||
return 'Scanning for vulnerabilities';
|
||||
case 'stopping':
|
||||
return 'Stopping';
|
||||
case 'removing':
|
||||
return 'Removing';
|
||||
case 'creating':
|
||||
return 'Creating';
|
||||
case 'starting':
|
||||
return 'Starting';
|
||||
case 'done':
|
||||
return 'Done';
|
||||
case 'updated':
|
||||
return 'Updated';
|
||||
case 'failed':
|
||||
return 'Failed';
|
||||
case 'blocked':
|
||||
return 'Blocked by vulnerabilities';
|
||||
case 'checked':
|
||||
return 'Checked';
|
||||
case 'skipped':
|
||||
return 'Up-to-date';
|
||||
default:
|
||||
return step;
|
||||
}
|
||||
}
|
||||
|
||||
// Get color classes for a step
|
||||
export function getStepColor(step: StepType): string {
|
||||
switch (step) {
|
||||
case 'done':
|
||||
case 'updated':
|
||||
return 'text-green-600 dark:text-green-400';
|
||||
case 'failed':
|
||||
return 'text-red-600 dark:text-red-400';
|
||||
case 'blocked':
|
||||
return 'text-amber-600 dark:text-amber-400';
|
||||
case 'scanning':
|
||||
return 'text-purple-600 dark:text-purple-400';
|
||||
case 'checked':
|
||||
case 'skipped':
|
||||
return 'text-muted-foreground';
|
||||
default:
|
||||
return 'text-blue-600 dark:text-blue-400';
|
||||
}
|
||||
}
|
||||
|
||||
// Vulnerability criteria labels
|
||||
export const vulnerabilityCriteriaLabels: Record<VulnerabilityCriteria, string> = {
|
||||
never: 'Never block',
|
||||
any: 'Any vulnerability',
|
||||
critical_high: 'Critical or high',
|
||||
critical: 'Critical only',
|
||||
more_than_current: 'More than current image'
|
||||
};
|
||||
|
||||
// Vulnerability criteria icons with colors and titles
|
||||
export const vulnerabilityCriteriaIcons: Record<
|
||||
VulnerabilityCriteria,
|
||||
{ component: ComponentType; class: string; title: string }
|
||||
> = {
|
||||
never: { component: ShieldOff, class: 'w-3.5 h-3.5 text-muted-foreground', title: 'No vulnerability blocking' },
|
||||
any: { component: ShieldAlert, class: 'w-3.5 h-3.5 text-amber-500', title: 'Block on any vulnerability' },
|
||||
critical_high: { component: ShieldX, class: 'w-3.5 h-3.5 text-orange-500', title: 'Block on critical or high' },
|
||||
critical: { component: ShieldX, class: 'w-3.5 h-3.5 text-red-500', title: 'Block on critical only' },
|
||||
more_than_current: { component: Shield, class: 'w-3.5 h-3.5 text-blue-500', title: 'Block if more than current' }
|
||||
};
|
||||
|
||||
// Get badge variant based on criteria severity
|
||||
export function getCriteriaBadgeClass(criteria: VulnerabilityCriteria): string {
|
||||
switch (criteria) {
|
||||
case 'any':
|
||||
return 'bg-red-500/10 text-red-600 border-red-500/30';
|
||||
case 'critical_high':
|
||||
return 'bg-orange-500/10 text-orange-600 border-orange-500/30';
|
||||
case 'critical':
|
||||
return 'bg-amber-500/10 text-amber-600 border-amber-500/30';
|
||||
case 'more_than_current':
|
||||
return 'bg-blue-500/10 text-blue-600 border-blue-500/30';
|
||||
default:
|
||||
return 'bg-slate-500/10 text-slate-600 border-slate-500/30';
|
||||
}
|
||||
}
|
||||
35
lib/utils/version.ts
Normal file
35
lib/utils/version.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Compares two semantic version strings.
|
||||
* @returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
||||
*/
|
||||
export function compareVersions(v1: string, v2: string): number {
|
||||
const normalize = (v: string) =>
|
||||
v
|
||||
.replace(/^v/, '')
|
||||
.split('.')
|
||||
.map(Number);
|
||||
const parts1 = normalize(v1);
|
||||
const parts2 = normalize(v2);
|
||||
|
||||
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
||||
const p1 = parts1[i] || 0;
|
||||
const p2 = parts2[i] || 0;
|
||||
if (p1 > p2) return 1;
|
||||
if (p1 < p2) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the "What's New" popup should be shown.
|
||||
* @param currentVersion - The current app version (from git tag)
|
||||
* @param lastSeenVersion - The last version the user has seen (from localStorage)
|
||||
*/
|
||||
export function shouldShowWhatsNew(
|
||||
currentVersion: string | null,
|
||||
lastSeenVersion: string | null
|
||||
): boolean {
|
||||
if (!currentVersion || currentVersion === 'unknown') return false;
|
||||
if (!lastSeenVersion) return true; // Never seen any version
|
||||
return compareVersions(currentVersion, lastSeenVersion) > 0;
|
||||
}
|
||||
Reference in New Issue
Block a user