dslinter 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/bin/lib/infer-prop-types-from-ts.mjs +14 -1
  3. package/bin/lib/infer-prop-types-from-ts.test.mjs +32 -0
  4. package/dashboard-dist/assets/{DashboardLayoutAuto-h0gP_iKd.js → DashboardLayoutAuto-B4P-sy4z.js} +1 -1
  5. package/dashboard-dist/assets/DashboardLayoutAuto-CAO6F6-q.css +1 -0
  6. package/dashboard-dist/assets/{axe-DDaE9JTN.js → axe-CaxTXfM9.js} +1 -1
  7. package/dashboard-dist/assets/index-B432JkIx.js +219 -0
  8. package/dashboard-dist/assets/index-D0O_5w5V.css +1 -0
  9. package/dashboard-dist/dslinter-report.json +23929 -0
  10. package/dashboard-dist/index.html +2 -2
  11. package/index.cjs +52 -52
  12. package/package.json +6 -6
  13. package/src/components/CatalogPane.tsx +94 -0
  14. package/src/components/ComponentInspectPane.tsx +125 -125
  15. package/src/components/ComponentPlaygroundPane.tsx +42 -27
  16. package/src/components/DashboardCommandPalette.tsx +9 -0
  17. package/src/components/GovernanceInventoryTabs.tsx +51 -0
  18. package/src/components/GovernancePane.tsx +18 -5
  19. package/src/components/PlaygroundA11yAndCode.tsx +0 -52
  20. package/src/components/PlaygroundControlField.tsx +2 -0
  21. package/src/components/ScoreGauge.test.ts +22 -0
  22. package/src/components/ScoreGauge.tsx +179 -0
  23. package/src/components/Sidebar.tsx +97 -23
  24. package/src/components/TokensPane.tsx +11 -13
  25. package/src/components/controlApiTable.test.ts +15 -0
  26. package/src/components/controlApiTable.ts +4 -0
  27. package/src/components/ui/badge.tsx +5 -5
  28. package/src/dashboard/ComponentCatalog.tsx +10 -1
  29. package/src/dashboard/ComponentPropUsageDetail.tsx +127 -42
  30. package/src/dashboard/ComponentUsageDetails.tsx +39 -9
  31. package/src/dashboard/DashboardBody.tsx +83 -12
  32. package/src/dashboard/ScannedTokenWall.tsx +9 -6
  33. package/src/dashboard/UnusedComponentsList.tsx +74 -0
  34. package/src/dashboard/aggregate.test.ts +381 -12
  35. package/src/dashboard/aggregate.ts +167 -30
  36. package/src/dashboard/mergeTokenCatalog.ts +5 -0
  37. package/src/dashboard/paths.test.ts +18 -1
  38. package/src/dashboard/paths.ts +8 -0
  39. package/src/mcp/agent-query.ts +1 -1
  40. package/src/playground/buildPlaygroundEntriesFromReport.test.ts +3 -3
  41. package/src/playground/controls.ts +16 -3
  42. package/src/playground/enrichKitControls.ts +5 -5
  43. package/src/playground/inferKitJsx.test.ts +0 -11
  44. package/src/playground/inferPropTypesFromTs.d.mts +1 -1
  45. package/src/playground/inferPropTypesFromTs.mjs +19 -3
  46. package/src/playground/inferPropTypesFromTs.test.ts +32 -0
  47. package/src/playground/inferPropTypesFromTs.ts +1 -1
  48. package/src/playground/playgroundJoin.ts +34 -0
  49. package/src/playground/propCoerce.ts +2 -2
  50. package/src/playground/snippet.ts +1 -0
  51. package/src/shell/DashboardLayout.tsx +21 -4
  52. package/src/shell/hashRoute.test.ts +9 -0
  53. package/src/shell/hashRoute.ts +6 -0
  54. package/src/types/controls.ts +12 -0
  55. package/src/types/report.ts +3 -2
  56. package/vite/embedTailwindSources.ts +8 -6
  57. package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +0 -1
  58. package/dashboard-dist/assets/index-B9sZ6wHm.css +0 -1
  59. package/dashboard-dist/assets/index-DIDBt5ed.js +0 -218
@@ -1,3 +1,13 @@
1
+ import { Badge } from "../components/ui/badge";
2
+ import {
3
+ Table,
4
+ TableBody,
5
+ TableCell,
6
+ TableHead,
7
+ TableHeader,
8
+ TableRow,
9
+ } from "../components/ui/table";
10
+ import { cn } from "../lib/utils";
1
11
  import type { WorkspaceReport } from "../types/report";
2
12
  import { pluralize } from "usemods";
3
13
 
@@ -8,6 +18,16 @@ export function catalogAttributeProps(props: string[]): string[] {
8
18
  return props.filter((prop) => !SLOT_PROPS.has(prop));
9
19
  }
10
20
 
21
+ export function propFrequenciesForComponent(
22
+ report: WorkspaceReport,
23
+ componentName: string,
24
+ ): Record<string, number> {
25
+ const usageRow = (report.usage_by_component ?? []).find(
26
+ (u) => u.component === componentName,
27
+ );
28
+ return usageRow?.prop_frequencies ?? {};
29
+ }
30
+
11
31
  /** Set of `"ComponentName/propName"` keys for props with no recorded usage. */
12
32
  export function buildUnusedPropSetForComponent(
13
33
  report: WorkspaceReport,
@@ -15,10 +35,7 @@ export function buildUnusedPropSetForComponent(
15
35
  declared: string[],
16
36
  ): Set<string> {
17
37
  const s = new Set<string>();
18
- const usageRow = (report.usage_by_component ?? []).find(
19
- (u) => u.component === componentName,
20
- );
21
- const propFrequencies = usageRow?.prop_frequencies ?? {};
38
+ const propFrequencies = propFrequenciesForComponent(report, componentName);
22
39
  for (const propName of catalogAttributeProps(declared)) {
23
40
  if ((propFrequencies[propName] ?? 0) === 0) {
24
41
  s.add(`${componentName}/${propName}`);
@@ -27,23 +44,106 @@ export function buildUnusedPropSetForComponent(
27
44
  return s;
28
45
  }
29
46
 
47
+ function sortedAttributeProps(declared: string[]): string[] {
48
+ return [...catalogAttributeProps(declared)].sort((a, b) =>
49
+ a.localeCompare(b),
50
+ );
51
+ }
52
+
53
+ function PropUsageSummary({
54
+ usedPropCount,
55
+ totalPropCount,
56
+ }: {
57
+ usedPropCount: number;
58
+ totalPropCount: number;
59
+ }) {
60
+ return (
61
+ <p className="text-sm text-muted-foreground">
62
+ {usedPropCount}/{totalPropCount} {pluralize("prop", usedPropCount)} used
63
+ in the workspace snapshot.
64
+ </p>
65
+ );
66
+ }
67
+
68
+ function PropUsageTable({
69
+ props,
70
+ propFrequencies,
71
+ }: {
72
+ props: string[];
73
+ propFrequencies: Record<string, number>;
74
+ }) {
75
+ return (
76
+ <Table>
77
+ <TableHeader>
78
+ <TableRow>
79
+ <TableHead>Prop</TableHead>
80
+ <TableHead className="w-20 text-right">Uses</TableHead>
81
+ </TableRow>
82
+ </TableHeader>
83
+ <TableBody>
84
+ {props.map((prop) => {
85
+ const count = propFrequencies[prop] ?? 0;
86
+ const isUnused = count === 0;
87
+ return (
88
+ <TableRow
89
+ key={prop}
90
+ className={isUnused ? "text-muted-foreground" : undefined}
91
+ >
92
+ <TableCell className="font-mono text-xs">{prop}</TableCell>
93
+ <TableCell className="text-right tabular-nums">{count}</TableCell>
94
+ </TableRow>
95
+ );
96
+ })}
97
+ </TableBody>
98
+ </Table>
99
+ );
100
+ }
101
+
102
+ function PropUsageBadges({
103
+ props,
104
+ propFrequencies,
105
+ }: {
106
+ props: string[];
107
+ propFrequencies: Record<string, number>;
108
+ }) {
109
+ return (
110
+ <div className="flex flex-wrap gap-1">
111
+ {props.map((prop) => {
112
+ const count = propFrequencies[prop] ?? 0;
113
+ const isUsed = count > 0;
114
+ return (
115
+ <Badge
116
+ key={prop}
117
+ variant={isUsed ? "secondary" : "outline"}
118
+ size="sm"
119
+ className={cn(!isUsed && "text-muted-foreground")}
120
+ title={isUsed ? `${count} uses` : "Never passed"}
121
+ >
122
+ {prop}
123
+ </Badge>
124
+ );
125
+ })}
126
+ </div>
127
+ );
128
+ }
129
+
30
130
  export function ComponentPropUsageDetail({
31
131
  component,
32
132
  declared,
33
133
  unusedProps,
134
+ propFrequencies = {},
135
+ variant = "table",
34
136
  }: {
35
137
  component: string;
36
138
  declared: string[];
37
139
  unusedProps: Set<string>;
140
+ propFrequencies?: Record<string, number>;
141
+ variant?: "table" | "compact";
38
142
  }) {
39
- const attributeProps = catalogAttributeProps(declared);
40
- const used = attributeProps.filter(
143
+ const attributeProps = sortedAttributeProps(declared);
144
+ const usedPropCount = attributeProps.filter(
41
145
  (prop) => !unusedProps.has(`${component}/${prop}`),
42
- );
43
- const unused = attributeProps.filter((prop) =>
44
- unusedProps.has(`${component}/${prop}`),
45
- );
46
- const usedPropCount = used.length;
146
+ ).length;
47
147
 
48
148
  if (attributeProps.length === 0) {
49
149
  return (
@@ -54,37 +154,22 @@ export function ComponentPropUsageDetail({
54
154
  }
55
155
 
56
156
  return (
57
- <div className="space-y-4">
58
- <p className="text-sm text-muted-foreground">
59
- {usedPropCount}/{attributeProps.length}{" "}
60
- {pluralize("prop", usedPropCount)} used in the workspace snapshot.
61
- </p>
62
- {used.length > 0 ? (
63
- <div>
64
- <p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
65
- Used
66
- </p>
67
- <ul className="mt-1 space-y-0.5 font-mono text-xs text-foreground">
68
- {used.map((prop) => (
69
- <li key={prop}>{prop}</li>
70
- ))}
71
- </ul>
72
- </div>
73
- ) : null}
74
- {unused.length > 0 ? (
75
- <div>
76
- <p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
77
- Never passed
78
- </p>
79
- <ul className="mt-1 space-y-0.5 font-mono text-xs text-muted-foreground/70">
80
- {unused.map((prop) => (
81
- <li key={prop} className="line-through">
82
- {prop}
83
- </li>
84
- ))}
85
- </ul>
86
- </div>
87
- ) : null}
157
+ <div className={variant === "compact" ? "space-y-2" : "space-y-4"}>
158
+ <PropUsageSummary
159
+ usedPropCount={usedPropCount}
160
+ totalPropCount={attributeProps.length}
161
+ />
162
+ {variant === "compact" ? (
163
+ <PropUsageBadges
164
+ props={attributeProps}
165
+ propFrequencies={propFrequencies}
166
+ />
167
+ ) : (
168
+ <PropUsageTable
169
+ props={attributeProps}
170
+ propFrequencies={propFrequencies}
171
+ />
172
+ )}
88
173
  </div>
89
174
  );
90
175
  }
@@ -1,4 +1,4 @@
1
- import { useMemo } from "react";
1
+ import { useCallback, useMemo } from "react";
2
2
  import {
3
3
  Table,
4
4
  TableBody,
@@ -9,7 +9,8 @@ import {
9
9
  } from "../components/ui/table";
10
10
  import type { UsageLocation, WorkspaceReport } from "../types/report";
11
11
  import { usageMap } from "./aggregate";
12
- import { shortPath } from "./paths";
12
+ import { openSourceFile } from "./editorLink";
13
+ import { resolveReportAbsolutePath, shortPath } from "./paths";
13
14
  import { EmptyCard } from "../components/EmptyCard";
14
15
  import { TruncatedPath } from "../components/TruncatedPath";
15
16
 
@@ -32,6 +33,40 @@ function sortedLocations(
32
33
  return list;
33
34
  }
34
35
 
36
+ function UsageLocationLink({
37
+ root,
38
+ loc,
39
+ }: {
40
+ root: string;
41
+ loc: UsageLocation;
42
+ }) {
43
+ const fileText = shortPath(root, loc.path);
44
+ const locationText = `${fileText}:${loc.line}`;
45
+ const absolutePath = resolveReportAbsolutePath(root, loc.path);
46
+
47
+ const handleClick = useCallback(() => {
48
+ void openSourceFile(absolutePath, loc.line).catch((err) => {
49
+ const message = err instanceof Error ? err.message : String(err);
50
+ window.alert(`Could not open file: ${message}`);
51
+ });
52
+ }, [absolutePath, loc.line]);
53
+
54
+ return (
55
+ <button
56
+ type="button"
57
+ onClick={handleClick}
58
+ className="block min-w-0 w-full text-left text-xs text-muted-foreground transition-colors hover:text-foreground hover:underline"
59
+ title={locationText}
60
+ >
61
+ <TruncatedPath
62
+ path={`${fileText}:${loc.line}`}
63
+ className="text-xs"
64
+ title={undefined}
65
+ />
66
+ </button>
67
+ );
68
+ }
69
+
35
70
  export function ComponentUsageDetails({
36
71
  report,
37
72
  componentId,
@@ -72,22 +107,17 @@ export function ComponentUsageDetails({
72
107
  <Table className="[&>table]:table-fixed [&>table]:w-full">
73
108
  <TableHeader>
74
109
  <TableRow>
75
- <TableHead className="w-[40%] min-w-0">File</TableHead>
76
- <TableHead className="w-14 whitespace-nowrap">Line</TableHead>
110
+ <TableHead className="w-[40%] min-w-0">Location</TableHead>
77
111
  <TableHead className="min-w-0">Props at this call site</TableHead>
78
112
  </TableRow>
79
113
  </TableHeader>
80
114
  <TableBody>
81
115
  {rows.map((loc, i) => {
82
- const fileText = shortPath(report.root, loc.path);
83
116
  const propsText = formatCallSiteProps(loc);
84
117
  return (
85
118
  <TableRow key={`${loc.path}-${loc.line}-${i}`}>
86
119
  <TableCell className="min-w-0">
87
- <TruncatedPath path={fileText} className="text-xs" />
88
- </TableCell>
89
- <TableCell className="tabular-nums text-muted-foreground">
90
- {loc.line}
120
+ <UsageLocationLink root={report.root} loc={loc} />
91
121
  </TableCell>
92
122
  <TableCell className="min-w-0">
93
123
  <span
@@ -1,16 +1,71 @@
1
+ import { useMemo, useState } from "react";
2
+ import { GovernanceInventoryTabs } from "../components/GovernanceInventoryTabs";
1
3
  import { Section } from "../components/Section";
2
4
  import type { WorkspaceReport } from "../types/report";
3
- import { ComponentCatalog } from "./ComponentCatalog";
5
+ import {
6
+ findingsForGovernanceTab,
7
+ governanceTabCounts,
8
+ type GovernanceInventoryTab,
9
+ unusedComponentsFromReport,
10
+ } from "./aggregate";
4
11
  import { FindingsList } from "./FindingsList";
5
12
  import { ScoreStrip } from "./ScoreStrip";
13
+ import { UnusedComponentsList } from "./UnusedComponentsList";
14
+
15
+ const sectionMeta: Record<
16
+ GovernanceInventoryTab,
17
+ { id: string; title: string; description: string }
18
+ > = {
19
+ all: {
20
+ id: "issues",
21
+ title: "Issues",
22
+ description: "All findings from the workspace DSLinter report.",
23
+ },
24
+ a11y: {
25
+ id: "accessibility-issues",
26
+ title: "Accessibility issues",
27
+ description: "Findings from accessibility rules in the latest snapshot.",
28
+ },
29
+ code: {
30
+ id: "code-issues",
31
+ title: "Code quality issues",
32
+ description: "Findings from code quality rules in the latest snapshot.",
33
+ },
34
+ token: {
35
+ id: "token-issues",
36
+ title: "Token issues",
37
+ description: "Findings from design token rules in the latest snapshot.",
38
+ },
39
+ unused: {
40
+ id: "unused-components",
41
+ title: "Unused components",
42
+ description:
43
+ "Scanned definitions with no JSX references elsewhere in the workspace.",
44
+ },
45
+ };
6
46
 
7
47
  export function DashboardBody({
8
48
  report,
9
49
  onOpenComponent,
50
+ onOpenCatalog,
10
51
  }: {
11
52
  report: WorkspaceReport;
12
53
  onOpenComponent?: (name: string) => void;
54
+ onOpenCatalog?: () => void;
13
55
  }) {
56
+ const [tab, setTab] = useState<GovernanceInventoryTab>("all");
57
+
58
+ const counts = useMemo(() => governanceTabCounts(report), [report]);
59
+ const filteredFindings = useMemo(
60
+ () => findingsForGovernanceTab(report, tab),
61
+ [report, tab],
62
+ );
63
+ const unusedComponents = useMemo(
64
+ () => unusedComponentsFromReport(report),
65
+ [report],
66
+ );
67
+ const section = sectionMeta[tab];
68
+
14
69
  return (
15
70
  <div className="space-y-10">
16
71
  <ScoreStrip scores={report.scores} />
@@ -22,21 +77,37 @@ export function DashboardBody({
22
77
  </div>
23
78
  ) : null}
24
79
 
25
- <Section
26
- id="components"
27
- title="Components"
28
- description="Definitions and JSX usage from the latest snapshot."
29
- >
30
- <ComponentCatalog report={report} onOpenComponent={onOpenComponent} />
31
- </Section>
80
+ <GovernanceInventoryTabs value={tab} onChange={setTab} counts={counts} />
32
81
 
33
82
  <Section
34
- id="issues"
35
- title="Issues"
36
- description="Findings from the workspace DSLinter report scoped to this file."
83
+ id={section.id}
84
+ title={section.title}
85
+ description={section.description}
37
86
  >
38
- <FindingsList findings={report.findings} root={report.root} />
87
+ {tab === "unused" ? (
88
+ <UnusedComponentsList
89
+ components={unusedComponents}
90
+ root={report.root}
91
+ onOpenComponent={onOpenComponent}
92
+ />
93
+ ) : (
94
+ <FindingsList findings={filteredFindings} root={report.root} />
95
+ )}
39
96
  </Section>
97
+
98
+ {onOpenCatalog ? (
99
+ <p className="text-sm text-muted-foreground">
100
+ <button
101
+ type="button"
102
+ onClick={onOpenCatalog}
103
+ className="font-medium text-foreground underline decoration-dotted underline-offset-2 transition hover:decoration-solid"
104
+ >
105
+ View all components
106
+ </button>
107
+ {" "}
108
+ for prop usage and app reference details.
109
+ </p>
110
+ ) : null}
40
111
  </div>
41
112
  );
42
113
  }
@@ -8,6 +8,7 @@ import { cn } from "../lib/utils";
8
8
  import { TruncatedPath } from "../components/TruncatedPath";
9
9
  import {
10
10
  filterTokenRows,
11
+ scannedTokenRowKey,
11
12
  type MergedTokenView,
12
13
  type ScannedTokenRow,
13
14
  type TokenUsageFilter,
@@ -40,13 +41,13 @@ function TokenSection({
40
41
  function TokenUsageBadge({ row }: { row: ScannedTokenRow }) {
41
42
  if (row.isUnused) {
42
43
  return (
43
- <span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
44
+ <span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
44
45
  unused
45
46
  </span>
46
47
  );
47
48
  }
48
49
  return (
49
- <span className="shrink-0 rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-primary">
50
+ <span className="shrink-0 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
50
51
  {row.fileCount} {row.fileCount === 1 ? "file" : "files"}
51
52
  </span>
52
53
  );
@@ -95,7 +96,9 @@ function TokenRowBody({
95
96
  }) {
96
97
  return (
97
98
  <div className={cn("min-w-0", className)}>
98
- <p className="truncate font-mono text-xs text-foreground">{row.cssName}</p>
99
+ <p className="truncate font-mono text-xs text-foreground">
100
+ {row.cssName}
101
+ </p>
99
102
  <p className="truncate text-xs text-muted-foreground">{row.value}</p>
100
103
  {row.tw ? (
101
104
  <p className="truncate font-mono text-xs text-muted-foreground/70">
@@ -118,8 +121,8 @@ function ColorSection({ rows }: { rows: ScannedTokenRow[] }) {
118
121
  <ul className="mt-4 grid gap-3 sm:grid-cols-2">
119
122
  {colors.map((row) => (
120
123
  <li
121
- key={row.cssName}
122
- className="flex items-center gap-3 rounded-lg border border-border bg-card px-3 py-2"
124
+ key={scannedTokenRowKey(row)}
125
+ className="flex items-center gap-3 rounded-lg border border-border bg-card p-2 pr-3.5"
123
126
  title={row.value}
124
127
  >
125
128
  {row.displayValue &&
@@ -162,7 +165,7 @@ function ListSection({
162
165
  <ul className="mt-3 divide-y divide-border rounded-lg border border-border bg-card">
163
166
  {items.map((row) => (
164
167
  <li
165
- key={row.cssName}
168
+ key={scannedTokenRowKey(row)}
166
169
  className="flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-xs"
167
170
  >
168
171
  <TokenRowBody row={row} className="flex-1" />
@@ -0,0 +1,74 @@
1
+ import {
2
+ Table,
3
+ TableBody,
4
+ TableCell,
5
+ TableHead,
6
+ TableHeader,
7
+ TableRow,
8
+ } from "../components/ui/table";
9
+ import { TruncatedPath } from "../components/TruncatedPath";
10
+ import type { UnusedComponent } from "./aggregate";
11
+ import { shortPath } from "./paths";
12
+ import { pluralize } from "usemods";
13
+
14
+ export function UnusedComponentsList({
15
+ components,
16
+ root,
17
+ onOpenComponent,
18
+ }: {
19
+ components: UnusedComponent[];
20
+ root: string;
21
+ onOpenComponent?: (name: string) => void;
22
+ }) {
23
+ if (components.length === 0) {
24
+ return (
25
+ <p className="rounded-lg border border-dashed border-border bg-muted/30 px-4 py-8 text-center text-sm text-muted-foreground">
26
+ No unused components — every scanned definition has at least one JSX
27
+ reference in the workspace.
28
+ </p>
29
+ );
30
+ }
31
+
32
+ return (
33
+ <Table>
34
+ <TableHeader>
35
+ <TableRow>
36
+ <TableHead scope="col">Component</TableHead>
37
+ <TableHead scope="col">Defined in</TableHead>
38
+ </TableRow>
39
+ </TableHeader>
40
+ <TableBody>
41
+ {components.map(({ name, definitionPaths }) => (
42
+ <TableRow key={name}>
43
+ <TableCell>
44
+ {onOpenComponent ? (
45
+ <button
46
+ type="button"
47
+ onClick={() => onOpenComponent(name)}
48
+ className="text-left font-medium text-foreground underline decoration-transparent underline-offset-2 transition hover:decoration-current"
49
+ >
50
+ {name}
51
+ </button>
52
+ ) : (
53
+ name
54
+ )}
55
+ </TableCell>
56
+ <TableCell className="min-w-0 font-mono text-xs text-muted-foreground">
57
+ {definitionPaths.length === 1 ? (
58
+ <TruncatedPath
59
+ path={shortPath(root, definitionPaths[0]!)}
60
+ className="text-xs"
61
+ />
62
+ ) : (
63
+ <span title={definitionPaths.map((p) => shortPath(root, p)).join("\n")}>
64
+ {definitionPaths.length}{" "}
65
+ {pluralize("file", definitionPaths.length)}
66
+ </span>
67
+ )}
68
+ </TableCell>
69
+ </TableRow>
70
+ ))}
71
+ </TableBody>
72
+ </Table>
73
+ );
74
+ }