File size: 4,786 Bytes
42ae0b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import { AlertCircle, AlertTriangle, CheckCircle2, Wrench } from "lucide-react";
import type { LintResult, LintViolation } from "@/lib/api";

interface LintPanelProps {
  result: LintResult | null;
  onJumpToLine?: (line: number) => void;
}

function SeverityBadge({ warning }: { warning: boolean }) {
  if (warning) {
    return (
      <span className="inline-flex items-center gap-1 text-[10px] font-medium px-1.5 py-0.5 rounded"
        style={{ background: "oklch(0.78 0.18 55 / 0.15)", color: "oklch(0.78 0.18 55)", border: "1px solid oklch(0.78 0.18 55 / 0.3)" }}>
        <AlertTriangle size={9} />
        WARN
      </span>
    );
  }
  return (
    <span className="inline-flex items-center gap-1 text-[10px] font-medium px-1.5 py-0.5 rounded"
      style={{ background: "oklch(0.60 0.20 25 / 0.15)", color: "oklch(0.65 0.20 25)", border: "1px solid oklch(0.60 0.20 25 / 0.3)" }}>
      <AlertCircle size={9} />
      ERROR
    </span>
  );
}

function RuleCodeBadge({ code }: { code: string }) {
  return (
    <span className="text-[10px] font-mono font-medium px-1.5 py-0.5 rounded"
      style={{ background: "oklch(0.68 0.16 210 / 0.12)", color: "oklch(0.68 0.16 210)", border: "1px solid oklch(0.68 0.16 210 / 0.25)" }}>
      {code}
    </span>
  );
}

function ViolationRow({ v, onJumpToLine }: { v: LintViolation; onJumpToLine?: (line: number) => void }) {
  return (
    <div
      className="group flex flex-col gap-1.5 px-3 py-2.5 border-b border-[oklch(0.18_0.010_264)] hover:bg-[oklch(0.16_0.010_264)] cursor-pointer transition-colors"
      onClick={() => onJumpToLine?.(v.line_no)}
    >
      <div className="flex items-center gap-2 flex-wrap">
        <SeverityBadge warning={v.warning} />
        <RuleCodeBadge code={v.code} />
        <span className="text-[10px] font-mono text-[oklch(0.45_0.010_264)] ml-auto group-hover:text-[oklch(0.55_0.010_264)] transition-colors">
          L{v.line_no}:{v.line_pos}
        </span>
        {v.fixable && (
          <span className="inline-flex items-center gap-0.5 text-[10px]"
            style={{ color: "oklch(0.72 0.17 160)" }}>
            <Wrench size={9} />
            fixable
          </span>
        )}
      </div>
      <p className="text-xs text-[oklch(0.78_0.010_264)] leading-relaxed">{v.description}</p>
    </div>
  );
}

export function LintPanel({ result, onJumpToLine }: LintPanelProps) {
  if (!result) {
    return (
      <div className="flex flex-col items-center justify-center h-full gap-3 text-[oklch(0.45_0.010_264)]">
        <AlertCircle size={32} className="opacity-30" />
        <p className="text-sm">Run analysis to see lint results</p>
      </div>
    );
  }

  const { violations, passed, stats } = result;

  return (
    <div className="flex flex-col h-full">
      {/* Summary bar */}
      <div className="flex items-center gap-4 px-3 py-2 border-b border-[oklch(0.22_0.010_264)] flex-shrink-0">
        {passed ? (
          <div className="flex items-center gap-1.5 text-xs" style={{ color: "oklch(0.72 0.17 160)" }}>
            <CheckCircle2 size={14} />
            <span className="font-medium">All checks passed</span>
          </div>
        ) : (
          <div className="flex items-center gap-1.5 text-xs" style={{ color: "oklch(0.65 0.20 25)" }}>
            <AlertCircle size={14} />
            <span className="font-medium">{stats.total} violation{stats.total !== 1 ? "s" : ""}</span>
          </div>
        )}
        <div className="flex items-center gap-3 ml-auto text-[10px] text-[oklch(0.45_0.010_264)]">
          {stats.errors > 0 && (
            <span style={{ color: "oklch(0.65 0.20 25)" }}>{stats.errors} error{stats.errors !== 1 ? "s" : ""}</span>
          )}
          {stats.warnings > 0 && (
            <span style={{ color: "oklch(0.78 0.18 55)" }}>{stats.warnings} warning{stats.warnings !== 1 ? "s" : ""}</span>
          )}
          {stats.fixable > 0 && (
            <span style={{ color: "oklch(0.72 0.17 160)" }}>{stats.fixable} fixable</span>
          )}
        </div>
      </div>

      {/* Violations list */}
      {passed ? (
        <div className="flex flex-col items-center justify-center h-full gap-3">
          <CheckCircle2 size={40} style={{ color: "oklch(0.72 0.17 160)" }} className="opacity-60" />
          <p className="text-sm font-medium" style={{ color: "oklch(0.72 0.17 160)" }}>No violations found</p>
          <p className="text-xs text-[oklch(0.45_0.010_264)]">Your SQL is clean for dialect: {result.dialect}</p>
        </div>
      ) : (
        <div className="flex-1 overflow-auto">
          {violations.map((v, i) => (
            <ViolationRow key={`${v.code}-${v.line_no}-${v.line_pos}-${i}`} v={v} onJumpToLine={onJumpToLine} />
          ))}
        </div>
      )}
    </div>
  );
}