feat: handle template conflict error
This commit is contained in:
@@ -56,36 +56,21 @@ func fetchTemplate(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, template)
|
||||
}
|
||||
|
||||
func createOrUpdateTemplate(c echo.Context) error {
|
||||
mgr := templateManagerFrom(c)
|
||||
exists, err := mgr.hasTemplate(c.Request().Context(), c.Param("templateName"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return createTemplate(c)
|
||||
}
|
||||
|
||||
var body postTemplateRequestBody
|
||||
err = json.NewDecoder(c.Request().Body).Decode(&body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if body.ImageTag != nil || body.BuildArgs != nil {
|
||||
return buildTemplate(c, body)
|
||||
}
|
||||
|
||||
return updateTemplate(c, body)
|
||||
}
|
||||
|
||||
func createTemplate(c echo.Context) error {
|
||||
mgr := templateManagerFrom(c)
|
||||
name := c.Param("templateName")
|
||||
mgr := templateManagerFrom(c)
|
||||
|
||||
exists, err := mgr.hasTemplate(c.Request().Context(), name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
return echo.NewHTTPError(http.StatusConflict)
|
||||
}
|
||||
|
||||
var body createTemplateRequestBody
|
||||
err := json.NewDecoder(c.Request().Body).Decode(&body)
|
||||
if err != nil {
|
||||
if err = json.NewDecoder(c.Request().Body).Decode(&body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -101,6 +86,29 @@ func createTemplate(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, createdTemplate)
|
||||
}
|
||||
|
||||
func updateOrBuildTemplate(c echo.Context) error {
|
||||
mgr := templateManagerFrom(c)
|
||||
exists, err := mgr.hasTemplate(c.Request().Context(), c.Param("templateName"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
|
||||
var body postTemplateRequestBody
|
||||
err = json.NewDecoder(c.Request().Body).Decode(&body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if body.ImageTag != nil || body.BuildArgs != nil {
|
||||
return buildTemplate(c, body)
|
||||
}
|
||||
|
||||
return updateTemplate(c, body)
|
||||
}
|
||||
|
||||
func updateTemplate(c echo.Context, body postTemplateRequestBody) error {
|
||||
name := c.Param("templateName")
|
||||
mgr := templateManagerFrom(c)
|
||||
|
@@ -9,7 +9,8 @@ func DefineRoutes(g *echo.Group, services service.Services) {
|
||||
g.Use(newTemplateManagerMiddleware(services))
|
||||
g.GET("/templates", fetchAllTemplates)
|
||||
g.GET("/templates/:templateName", fetchTemplate, validateTemplateName)
|
||||
g.POST("/templates/:templateName", createOrUpdateTemplate, validateTemplateName)
|
||||
g.PUT("/templates/:templateName", createTemplate, validateTemplateName)
|
||||
g.POST("/templates/:templateName", updateOrBuildTemplate, validateTemplateName)
|
||||
g.DELETE("/templates/:templateName", deleteTemplate, validateTemplateName)
|
||||
g.GET("/templates/:templateName/:filePath", fetchTemplateFile, validateTemplateName, validateTemplateFilePath)
|
||||
g.POST("/templates/:templateName/:filePath", updateTemplateFile, validateTemplateName, validateTemplateFilePath)
|
||||
|
@@ -41,6 +41,7 @@ type buildTemplateOptions struct {
|
||||
}
|
||||
|
||||
var errTemplateNotFound = errors.New("template not found")
|
||||
var errTemplateExists = errors.New("template already exists")
|
||||
var errBaseTemplateNotFound = errors.New("base template not found")
|
||||
var errTemplateFileNotFound = errors.New("template file not found")
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { promiseOrThrow } from "./lib/errors";
|
||||
|
||||
interface ApiErrorResponse {
|
||||
interface ApiErrorDetails {
|
||||
code: string;
|
||||
error: string;
|
||||
}
|
||||
@@ -8,12 +8,13 @@ interface ApiErrorResponse {
|
||||
const API_ERROR_BAD_TEMPLATE = "BAD_TEMPLATE";
|
||||
const API_ERROR_WORKSPACE_EXISTS = "WORKSPACE_EXISTS";
|
||||
|
||||
enum ApiError {
|
||||
NotFound = "NOT_FOUND",
|
||||
BadRequest = "BAD_REQUEST",
|
||||
Internal = "INTERNAL",
|
||||
Network = "NETWORK",
|
||||
}
|
||||
type ApiError =
|
||||
| { type: "NOT_FOUND" }
|
||||
| { type: "NETWORK" }
|
||||
| { type: "BAD_REQUEST" }
|
||||
| { type: "CONFLICT" }
|
||||
| { type: "INTERNAL" }
|
||||
| { type: "BAD_REQUEST"; details: ApiErrorDetails };
|
||||
|
||||
async function fetchApi(
|
||||
url: URL | RequestInfo,
|
||||
@@ -21,22 +22,27 @@ async function fetchApi(
|
||||
): Promise<Response> {
|
||||
const res = await promiseOrThrow(
|
||||
fetch(`${import.meta.env.VITE_API_URL}/api${url}`, init),
|
||||
() => ApiError.Network,
|
||||
() => ({ type: "NETWORK" }),
|
||||
);
|
||||
if (res.status !== 200) {
|
||||
switch (res.status) {
|
||||
case 400:
|
||||
throw await res.json();
|
||||
throw {
|
||||
type: "BAD_REQUEST",
|
||||
details: await res.json(),
|
||||
};
|
||||
case 404:
|
||||
throw ApiError.NotFound;
|
||||
throw { type: "NOT_FOUND" };
|
||||
case 409:
|
||||
throw { type: "CONFLICT" };
|
||||
default:
|
||||
throw ApiError.Internal;
|
||||
throw { type: "INTERNAL" };
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function isApiErrorResponse(error: unknown): error is ApiErrorResponse {
|
||||
function isApiErrorResponse(error: unknown): error is ApiErrorDetails {
|
||||
return (
|
||||
error !== null &&
|
||||
error !== undefined &&
|
||||
@@ -49,8 +55,7 @@ function isApiErrorResponse(error: unknown): error is ApiErrorResponse {
|
||||
export {
|
||||
API_ERROR_BAD_TEMPLATE,
|
||||
API_ERROR_WORKSPACE_EXISTS,
|
||||
ApiError,
|
||||
fetchApi,
|
||||
isApiErrorResponse,
|
||||
};
|
||||
export type { ApiErrorResponse };
|
||||
export type { ApiError, ApiErrorDetails };
|
||||
|
@@ -6,7 +6,8 @@ import type {
|
||||
TemplateImage,
|
||||
BaseTemplate,
|
||||
} from "./types";
|
||||
import { fetchApi } from "@/api";
|
||||
import { ApiError, fetchApi } from "@/api";
|
||||
import { promiseOrThrow } from "@/lib/errors";
|
||||
|
||||
function useTemplates() {
|
||||
return useSWR(
|
||||
@@ -17,7 +18,7 @@ function useTemplates() {
|
||||
}
|
||||
|
||||
function useTemplate(name: string) {
|
||||
return useSWR(
|
||||
return useSWR<Template, ApiError>(
|
||||
["/templates", name],
|
||||
async (): Promise<Template> =>
|
||||
fetchApi(`/templates/${name}`).then((res) => res.json()),
|
||||
@@ -45,7 +46,7 @@ function useDeleteTemplate() {
|
||||
|
||||
function useCreateTemplate() {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [error, setError] = useState<unknown | null>(null);
|
||||
const [error, setError] = useState<ApiError | null>(null);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const createTemplate = useCallback(
|
||||
@@ -60,14 +61,17 @@ function useCreateTemplate() {
|
||||
}): Promise<Template | null> => {
|
||||
try {
|
||||
const res = await fetchApi(`/templates/${name}`, {
|
||||
method: "POST",
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ description, baseTemplate }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const template: Template = await res.json();
|
||||
const template = await promiseOrThrow<Template, ApiError>(
|
||||
res.json(),
|
||||
() => ({ type: "INTERNAL" }),
|
||||
);
|
||||
mutate(["/templates", name], template, {
|
||||
populateCache: (newTemplate) => newTemplate,
|
||||
revalidate: false,
|
||||
@@ -75,7 +79,7 @@ function useCreateTemplate() {
|
||||
|
||||
return template;
|
||||
} catch (err: unknown) {
|
||||
setError(err);
|
||||
setError(err as ApiError);
|
||||
return null;
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
@@ -88,10 +92,12 @@ function useCreateTemplate() {
|
||||
}
|
||||
|
||||
function useTemplateFile(templateName: string, filePath: string) {
|
||||
return useSWR(filePath ? ["/templates", templateName, filePath] : null, () =>
|
||||
fetchApi(`/templates/${templateName}/${filePath}`).then((res) =>
|
||||
res.text(),
|
||||
),
|
||||
return useSWR<string, ApiError>(
|
||||
filePath ? ["/templates", templateName, filePath] : null,
|
||||
() =>
|
||||
fetchApi(`/templates/${templateName}/${filePath}`).then((res) =>
|
||||
res.text(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -30,6 +30,8 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import type { BaseTemplate } from "./types";
|
||||
import { useEffect } from "react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
const NewTemplateForm = object({
|
||||
baseTemplate: nonempty(string()),
|
||||
@@ -75,7 +77,8 @@ function TemplateFormContainer() {
|
||||
|
||||
function TemplateForm({ baseTemplates }: { baseTemplates: BaseTemplate[] }) {
|
||||
const router = useRouter();
|
||||
const { createTemplate, isCreatingTemplate } = useCreateTemplate();
|
||||
const { createTemplate, isCreatingTemplate, error } = useCreateTemplate();
|
||||
const { toast } = useToast();
|
||||
const form = useForm({
|
||||
resolver: superstructResolver(NewTemplateForm),
|
||||
disabled: isCreatingTemplate,
|
||||
@@ -86,6 +89,36 @@ function TemplateForm({ baseTemplates }: { baseTemplates: BaseTemplate[] }) {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) return;
|
||||
|
||||
switch (error.type) {
|
||||
case "CONFLICT":
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Template already exists",
|
||||
description: "Please use another name for the template",
|
||||
});
|
||||
break;
|
||||
|
||||
case "NETWORK":
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to create the template",
|
||||
description: "Network error",
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to create the template",
|
||||
description: "Unknown error",
|
||||
});
|
||||
break;
|
||||
}
|
||||
}, [error, toast]);
|
||||
|
||||
async function onSubmit(values: Infer<typeof NewTemplateForm>) {
|
||||
const createdTemplate = await createTemplate({
|
||||
name: values.templateName,
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { API_ERROR_BAD_TEMPLATE, ApiError } from "@/api";
|
||||
import { API_ERROR_BAD_TEMPLATE } from "@/api";
|
||||
import { CodeMirrorEditor } from "@/components/codemirror-editor";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import {
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
SidebarMenuItem,
|
||||
SidebarProvider,
|
||||
} from "@/components/ui/sidebar.tsx";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Link, useRouter } from "@tanstack/react-router";
|
||||
import { ArrowLeft, ChevronDown, ChevronUp, Loader2 } from "lucide-react";
|
||||
@@ -25,10 +27,8 @@ import {
|
||||
createTemplateEditorStore,
|
||||
useTemplateEditorStore,
|
||||
} from "./template-editor-store";
|
||||
import type { Template } from "./types";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { TemplateEditorTopBar } from "./template-editor-top-bar";
|
||||
import type { Template } from "./types";
|
||||
|
||||
function TemplateEditor() {
|
||||
const { templateName, _splat } = templateEditorRoute.useParams();
|
||||
@@ -43,13 +43,13 @@ function TemplateEditor() {
|
||||
}
|
||||
|
||||
if (error || !template) {
|
||||
if (error === ApiError.NotFound) {
|
||||
if (error?.type === "NOT_FOUND") {
|
||||
return <TemplateNotFound />;
|
||||
}
|
||||
|
||||
let message = "";
|
||||
switch (error) {
|
||||
case ApiError.Network:
|
||||
switch (error?.type) {
|
||||
case "NETWORK":
|
||||
message = "Having trouble contacting the server.";
|
||||
break;
|
||||
default:
|
||||
@@ -178,11 +178,11 @@ function Editor() {
|
||||
|
||||
if (error || fileContent === undefined) {
|
||||
let message = "";
|
||||
switch (error) {
|
||||
case ApiError.NotFound:
|
||||
switch (error?.type) {
|
||||
case "NOT_FOUND":
|
||||
message = "This file does not exist in the template.";
|
||||
break;
|
||||
case ApiError.Network:
|
||||
case "NETWORK":
|
||||
message = "Having trouble contacting the server.";
|
||||
break;
|
||||
default:
|
||||
|
Reference in New Issue
Block a user