Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7126aca51a | |||
| 658631abd8 | |||
| f9c15b84f0 |
@@ -2,6 +2,7 @@ import { API } from "api/api";
|
||||
import { checkAuthorization } from "api/queries/authCheck";
|
||||
import type { AuthorizationRequest } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Badge } from "components/Badge/Badge";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { Margins } from "components/Margins/Margins";
|
||||
import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs";
|
||||
@@ -50,10 +51,20 @@ const fetchTemplate = async (organizationId: string, templateName: string) => {
|
||||
}),
|
||||
]);
|
||||
|
||||
// TODO: Replace with actual API call once backend is implemented
|
||||
// const warnings = await API.getTemplateVersionWarnings(activeVersion.id);
|
||||
const warnings: Array<{ dismissed?: boolean }> = [
|
||||
{ dismissed: false },
|
||||
{ dismissed: false },
|
||||
{ dismissed: false },
|
||||
];
|
||||
const unreadWarningsCount = warnings.filter(w => !w.dismissed).length;
|
||||
|
||||
return {
|
||||
template,
|
||||
activeVersion,
|
||||
permissions,
|
||||
unreadWarningsCount,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -157,6 +168,16 @@ export const TemplateLayout: FC<PropsWithChildren> = ({
|
||||
Insights
|
||||
</TabLink>
|
||||
)}
|
||||
<TabLink to="warnings" value="warnings">
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
Warnings
|
||||
{data.unreadWarningsCount > 0 && (
|
||||
<Badge variant="destructive" size="xs">
|
||||
{data.unreadWarningsCount}
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
</TabLink>
|
||||
</TabsList>
|
||||
</Margins>
|
||||
</Tabs>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Template Warnings Page
|
||||
|
||||
This page displays warnings and errors for the active template version, providing tips and suggestions on how to better format Terraform code.
|
||||
|
||||
## Features
|
||||
|
||||
- **Table-based list view** with organized columns for easy scanning
|
||||
- **Minimize/Restore warnings** - click the X button to minimize warnings (moves to bottom) or ↻ to restore
|
||||
- **Smart sorting** - Active warnings at top, dismissed warnings at bottom
|
||||
- **Three severity levels**: Error, Warning, and Info with color-coded pills and icons
|
||||
- **Empty state** when no warnings exist
|
||||
- **Optional error codes** displayed in monospace badges
|
||||
- **Visual feedback** - Dismissed warnings show reduced opacity and simplified view
|
||||
- **Clean, professional design** following Coder's design system patterns
|
||||
|
||||
## Backend Integration
|
||||
|
||||
The page currently uses a placeholder `useTemplateWarnings` hook that returns an empty array. You'll need to:
|
||||
|
||||
1. **Create an API endpoint** in the Go backend that returns warnings for a template version
|
||||
2. **Add the API method** to `site/src/api/api.ts` (e.g., `getTemplateVersionWarnings`)
|
||||
3. **Replace the hook** with an actual React Query hook that fetches data
|
||||
|
||||
### Expected Warning Structure
|
||||
|
||||
```typescript
|
||||
interface Warning {
|
||||
id: string; // Unique identifier for dismissal tracking
|
||||
severity: "error" | "warning" | "info";
|
||||
title: string;
|
||||
message: string;
|
||||
code?: string; // Optional error code (e.g., "TF001")
|
||||
dismissed?: boolean; // Optional - marks warning as dismissed from backend
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The component tracks dismissed state both from:
|
||||
1. Backend-provided `dismissed` attribute (persisted across sessions)
|
||||
2. Client-side state (temporary, resets on page reload)
|
||||
|
||||
### Example Implementation
|
||||
|
||||
```typescript
|
||||
// In site/src/api/api.ts
|
||||
export const getTemplateVersionWarnings = async (
|
||||
versionId: string,
|
||||
): Promise<Warning[]> => {
|
||||
const response = await axios.get(
|
||||
`/api/v2/templateversions/${versionId}/warnings`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// In TemplateWarningsPage.tsx - replace the placeholder hook
|
||||
const useTemplateWarnings = (templateVersionId: string): Warning[] => {
|
||||
const { data } = useQuery({
|
||||
queryKey: ['templateWarnings', templateVersionId],
|
||||
queryFn: () => API.getTemplateVersionWarnings(templateVersionId),
|
||||
});
|
||||
return data ?? [];
|
||||
};
|
||||
```
|
||||
|
||||
## UI Components
|
||||
|
||||
The page uses Coder's standard table components:
|
||||
- `Table`, `TableHeader`, `TableBody`, `TableRow`, `TableCell`, `TableHead` from `components/Table/Table`
|
||||
- `Pill` component for severity badges
|
||||
- `EmptyState` component for empty states
|
||||
- Icons from `lucide-react` (CircleAlertIcon, TriangleAlertIcon, InfoIcon, XIcon, RotateCcwIcon)
|
||||
- Theme-aware colors and consistent typography
|
||||
|
||||
### Table Columns
|
||||
|
||||
1. **Icon** (40px) - Visual severity indicator (dimmed when dismissed)
|
||||
2. **Severity** (120px) - Colored pill with severity level (hidden when dismissed)
|
||||
3. **Issue** (flexible) - Warning title and detailed message (simplified when dismissed)
|
||||
4. **Code** (100px) - Optional error code in monospace (hidden when dismissed)
|
||||
5. **Actions** (40px) - Toggle button (X to dismiss, ↻ to restore)
|
||||
|
||||
### Dismissed State
|
||||
|
||||
When a warning is dismissed:
|
||||
- **Opacity reduced** to 50%
|
||||
- **Moved to bottom** of the list
|
||||
- **Severity pill hidden**
|
||||
- **Message hidden** (only title shown in italic)
|
||||
- **Code hidden**
|
||||
- **Icon dimmed**
|
||||
- **Button changes** to restore icon (↻)
|
||||
|
||||
## Navigation
|
||||
|
||||
The Warnings tab is visible to all users and appears after Insights in the template tabs:
|
||||
- Docs
|
||||
- Source Code (if user can update template)
|
||||
- Resources
|
||||
- Versions
|
||||
- Embed
|
||||
- Insights (if user has permissions)
|
||||
- **Warnings** (always visible)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for the backend implementation:
|
||||
|
||||
1. **Persist dismissed state**: Save dismissed warnings to backend so they persist across sessions
|
||||
2. **File locations**: Add file name and line number to each warning
|
||||
3. **Quick fixes**: Provide automated fix suggestions
|
||||
4. **Filtering**: Allow filtering by severity level (show/hide dismissed)
|
||||
5. **Categorization**: Group warnings by type (security, best practices, etc.)
|
||||
6. **Historical data**: Show warning trends across versions
|
||||
7. **Bulk actions**: Dismiss all warnings of a certain type
|
||||
@@ -0,0 +1,328 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "components/Table/Table";
|
||||
import {
|
||||
CircleAlertIcon,
|
||||
InfoIcon,
|
||||
RotateCcwIcon,
|
||||
TriangleAlertIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
|
||||
import { type FC, useMemo, useState } from "react";
|
||||
import { pageTitle } from "utils/page";
|
||||
|
||||
interface Warning {
|
||||
id: string;
|
||||
severity: "error" | "warning" | "info";
|
||||
title: string;
|
||||
message: string;
|
||||
code?: string;
|
||||
dismissed?: boolean;
|
||||
}
|
||||
|
||||
// Placeholder for backend data - the user will implement the actual API call
|
||||
const useTemplateWarnings = (templateVersionId: string): Warning[] => {
|
||||
// TODO: Replace this with actual API call once backend is implemented
|
||||
// Example: const { data } = useQuery(['templateWarnings', templateVersionId], () => API.getTemplateVersionWarnings(templateVersionId));
|
||||
|
||||
// Mock data for UI demonstration
|
||||
return [
|
||||
{
|
||||
id: "1",
|
||||
severity: "error",
|
||||
title: "Terraform module 'coder-server' is not pinned to a version",
|
||||
message:
|
||||
"The 'coder-server' module should be pinned to a specific version to ensure stability and reproducibility. " +
|
||||
"Please update the module source to include a version constraint.",
|
||||
code: "ERR_UNPINNED_MODULE_VERSION",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
severity: "error",
|
||||
title: "Missing terraform lock file",
|
||||
message:
|
||||
"The terraform lock file (terraform.lock.hcl) is missing. " +
|
||||
"Please generate the lock file to ensure consistent provider versions.",
|
||||
code: "ERR_MISSING_LOCK_FILE",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
severity: "warning",
|
||||
title: "Unused coder parameter 'region' detected",
|
||||
message: "The parameter 'region' is defined but not used in the template. ",
|
||||
code: "WRN_UNUSED_PARAMETER",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const TemplateWarningsPage: FC = () => {
|
||||
const { template, activeVersion } = useTemplateLayoutContext();
|
||||
const theme = useTheme();
|
||||
const warnings = useTemplateWarnings(activeVersion.id);
|
||||
const [dismissedWarnings, setDismissedWarnings] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
|
||||
// Sort warnings: non-dismissed first, dismissed at the end
|
||||
const sortedWarnings = useMemo(() => {
|
||||
return [...warnings].sort((a, b) => {
|
||||
const aDismissed = dismissedWarnings.has(a.id) || a.dismissed;
|
||||
const bDismissed = dismissedWarnings.has(b.id) || b.dismissed;
|
||||
|
||||
if (aDismissed === bDismissed) return 0;
|
||||
return aDismissed ? 1 : -1;
|
||||
});
|
||||
}, [warnings, dismissedWarnings]);
|
||||
|
||||
const handleToggleDismiss = (warningId: string) => {
|
||||
setDismissedWarnings((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(warningId)) {
|
||||
next.delete(warningId);
|
||||
} else {
|
||||
next.add(warningId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const isWarningDismissed = (warning: Warning) => {
|
||||
return dismissedWarnings.has(warning.id) || warning.dismissed;
|
||||
};
|
||||
|
||||
const getSeverityIcon = (severity: string, dismissed: boolean) => {
|
||||
const iconSize = 16;
|
||||
const opacity = dismissed ? 0.4 : 1;
|
||||
|
||||
switch (severity) {
|
||||
case "error":
|
||||
return (
|
||||
<CircleAlertIcon
|
||||
size={iconSize}
|
||||
css={{ color: theme.palette.error.main, opacity }}
|
||||
/>
|
||||
);
|
||||
case "warning":
|
||||
return (
|
||||
<TriangleAlertIcon
|
||||
size={iconSize}
|
||||
css={{ color: theme.palette.warning.main, opacity }}
|
||||
/>
|
||||
);
|
||||
case "info":
|
||||
default:
|
||||
return (
|
||||
<InfoIcon
|
||||
size={iconSize}
|
||||
css={{ color: theme.palette.info.main, opacity }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverityPillType = (
|
||||
severity: string,
|
||||
): "error" | "warning" | "info" => {
|
||||
switch (severity) {
|
||||
case "error":
|
||||
return "error";
|
||||
case "warning":
|
||||
return "warning";
|
||||
case "info":
|
||||
default:
|
||||
return "info";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{pageTitle(template.name, "Warnings")}</title>
|
||||
|
||||
<div
|
||||
css={{
|
||||
background: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
css={{
|
||||
padding: "16px 24px",
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
fontWeight: 600,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
Warnings & Errors
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
Showing diagnostics for the active template version:{" "}
|
||||
<span css={{ fontWeight: 600 }}>{activeVersion.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{sortedWarnings.length === 0 ? (
|
||||
<div css={{ padding: "64px 24px" }}>
|
||||
<EmptyState
|
||||
message="No warnings or errors"
|
||||
description="This template version looks good!"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead css={{ width: 40 }} />
|
||||
<TableHead css={{ width: 120 }}>Severity</TableHead>
|
||||
<TableHead>Issue</TableHead>
|
||||
<TableHead css={{ width: 100 }}>Code</TableHead>
|
||||
<TableHead css={{ width: 40 }} />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sortedWarnings.map((warning) => {
|
||||
const isDismissed = isWarningDismissed(warning);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={warning.id}
|
||||
css={{
|
||||
opacity: isDismissed ? 0.5 : 1,
|
||||
transition: "opacity 0.2s ease",
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<div css={{ display: "flex", alignItems: "center" }}>
|
||||
{getSeverityIcon(
|
||||
warning.severity,
|
||||
isDismissed || false,
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{!isDismissed && (
|
||||
<Pill type={getSeverityPillType(warning.severity)}>
|
||||
{warning.severity.charAt(0).toUpperCase() +
|
||||
warning.severity.slice(1)}
|
||||
</Pill>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isDismissed ? (
|
||||
<div
|
||||
css={{
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
{warning.title}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div
|
||||
css={{
|
||||
fontWeight: 600,
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.primary,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
{warning.title}
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{warning.message}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{!isDismissed && warning.code && (
|
||||
<span
|
||||
css={{
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
fontFamily: "monospace",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
padding: "2px 8px",
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
{warning.code}
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleToggleDismiss(warning.id)}
|
||||
css={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
padding: 4,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 4,
|
||||
color: theme.palette.text.secondary,
|
||||
transition: "all 0.15s ease",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
aria-label={
|
||||
isDismissed ? "Restore warning" : "Dismiss warning"
|
||||
}
|
||||
title={isDismissed ? "Restore" : "Dismiss"}
|
||||
>
|
||||
{isDismissed ? (
|
||||
<RotateCcwIcon size={16} />
|
||||
) : (
|
||||
<XIcon size={16} />
|
||||
)}
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateWarningsPage;
|
||||
@@ -287,6 +287,10 @@ const TemplateInsightsPage = lazy(
|
||||
() =>
|
||||
import("./pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage"),
|
||||
);
|
||||
const TemplateWarningsPage = lazy(
|
||||
() =>
|
||||
import("./pages/TemplatePage/TemplateWarningsPage/TemplateWarningsPage"),
|
||||
);
|
||||
const PremiumPage = lazy(
|
||||
() => import("./pages/DeploymentSettingsPage/PremiumPage/PremiumPage"),
|
||||
);
|
||||
@@ -361,6 +365,7 @@ const templateRouter = () => {
|
||||
<Route path="versions" element={<TemplateVersionsPage />} />
|
||||
<Route path="embed" element={<TemplateEmbedExperimentRouter />} />
|
||||
<Route path="insights" element={<TemplateInsightsPage />} />
|
||||
<Route path="warnings" element={<TemplateWarningsPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="workspace" element={<CreateWorkspaceExperimentRouter />} />
|
||||
|
||||
Reference in New Issue
Block a user