mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-10 20:02:26 +00:00
✨ blueprint details page
This commit is contained in:
@@ -1158,7 +1158,10 @@
|
|||||||
"blueprintGoBack": "Back to blueprints",
|
"blueprintGoBack": "Back to blueprints",
|
||||||
"blueprintCreate": "Create blueprint",
|
"blueprintCreate": "Create blueprint",
|
||||||
"blueprintCreateDescription2": "Follow the steps below to create and apply a new blueprint",
|
"blueprintCreateDescription2": "Follow the steps below to create and apply a new blueprint",
|
||||||
|
"blueprintDetails": "Blueprint details",
|
||||||
|
"blueprintDetailsDescription": "See the blueprint run details",
|
||||||
"blueprintInfo": "Blueprint Information",
|
"blueprintInfo": "Blueprint Information",
|
||||||
|
"message": "Message",
|
||||||
"blueprintNameDescription": "This is the display name for the blueprint.",
|
"blueprintNameDescription": "This is the display name for the blueprint.",
|
||||||
"blueprintContentsDescription": "Define the YAML content describing your infrastructure",
|
"blueprintContentsDescription": "Define the YAML content describing your infrastructure",
|
||||||
"blueprintErrorCreateDescription": "An error occurred when applying the blueprint",
|
"blueprintErrorCreateDescription": "An error occurred when applying the blueprint",
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export enum ActionsEnum {
|
|||||||
|
|
||||||
// blueprints
|
// blueprints
|
||||||
listBlueprints = "listBlueprints",
|
listBlueprints = "listBlueprints",
|
||||||
|
getBlueprint = "getBlueprint",
|
||||||
applyBlueprint = "applyBlueprint"
|
applyBlueprint = "applyBlueprint"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const registry = new OpenAPIRegistry();
|
|||||||
export enum OpenAPITags {
|
export enum OpenAPITags {
|
||||||
Site = "Site",
|
Site = "Site",
|
||||||
Org = "Organization",
|
Org = "Organization",
|
||||||
|
Blueprint = "Blueprint",
|
||||||
Resource = "Resource",
|
Resource = "Resource",
|
||||||
Role = "Role",
|
Role = "Role",
|
||||||
User = "User",
|
User = "User",
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ registry.registerPath({
|
|||||||
path: "/org/{orgId}/blueprint",
|
path: "/org/{orgId}/blueprint",
|
||||||
description:
|
description:
|
||||||
"Create and Apply a base64 encoded blueprint to an organization",
|
"Create and Apply a base64 encoded blueprint to an organization",
|
||||||
tags: [OpenAPITags.Org],
|
tags: [OpenAPITags.Org, OpenAPITags.Blueprint],
|
||||||
request: {
|
request: {
|
||||||
params: applyBlueprintParamsSchema,
|
params: applyBlueprintParamsSchema,
|
||||||
body: {
|
body: {
|
||||||
|
|||||||
110
server/routers/blueprints/getBlueprint.ts
Normal file
110
server/routers/blueprints/getBlueprint.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { blueprints, orgs } from "@server/db";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import stoi from "@server/lib/stoi";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { BlueprintData } from "./types";
|
||||||
|
|
||||||
|
const getBlueprintSchema = z
|
||||||
|
.object({
|
||||||
|
blueprintId: z
|
||||||
|
.string()
|
||||||
|
.transform(stoi)
|
||||||
|
.pipe(z.number().int().positive()),
|
||||||
|
orgId: z.string()
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
async function query(blueprintId: number, orgId: string) {
|
||||||
|
// Get the client
|
||||||
|
const [blueprint] = await db
|
||||||
|
.select({
|
||||||
|
blueprintId: blueprints.blueprintId,
|
||||||
|
name: blueprints.name,
|
||||||
|
source: blueprints.source,
|
||||||
|
succeeded: blueprints.succeeded,
|
||||||
|
orgId: blueprints.orgId,
|
||||||
|
createdAt: blueprints.createdAt,
|
||||||
|
message: blueprints.message,
|
||||||
|
contents: blueprints.contents
|
||||||
|
})
|
||||||
|
.from(blueprints)
|
||||||
|
.leftJoin(orgs, eq(blueprints.orgId, orgs.orgId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(blueprints.blueprintId, blueprintId),
|
||||||
|
eq(blueprints.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!blueprint) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return blueprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetBlueprintResponse = BlueprintData;
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/org/{orgId}/blueprint/{blueprintId}",
|
||||||
|
description: "Get a blueprint by its blueprint ID.",
|
||||||
|
tags: [OpenAPITags.Org, OpenAPITags.Blueprint],
|
||||||
|
request: {
|
||||||
|
params: getBlueprintSchema
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getBlueprint(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = getBlueprintSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
logger.error(
|
||||||
|
`Error parsing params: ${fromError(parsedParams.error).toString()}`
|
||||||
|
);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orgId, blueprintId } = parsedParams.data;
|
||||||
|
|
||||||
|
const blueprint = await query(blueprintId, orgId);
|
||||||
|
|
||||||
|
if (!blueprint) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, "Client not found")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response<GetBlueprintResponse>(res, {
|
||||||
|
data: blueprint as BlueprintData,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Client retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./listBlueprints";
|
export * from "./listBlueprints";
|
||||||
export * from "./createAndApplyBlueprint";
|
export * from "./createAndApplyBlueprint";
|
||||||
|
export * from "./getBlueprint";
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ async function queryBlueprints(orgId: string, limit: number, offset: number) {
|
|||||||
})
|
})
|
||||||
.from(blueprints)
|
.from(blueprints)
|
||||||
.leftJoin(orgs, eq(blueprints.orgId, orgs.orgId))
|
.leftJoin(orgs, eq(blueprints.orgId, orgs.orgId))
|
||||||
|
.where(eq(blueprints.orgId, orgId))
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset);
|
.offset(offset);
|
||||||
return res;
|
return res;
|
||||||
@@ -70,7 +71,7 @@ registry.registerPath({
|
|||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/blueprints",
|
path: "/org/{orgId}/blueprints",
|
||||||
description: "List all blueprints for a organization.",
|
description: "List all blueprints for a organization.",
|
||||||
tags: [OpenAPITags.Org],
|
tags: [OpenAPITags.Org, OpenAPITags.Blueprint],
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
@@ -121,10 +122,8 @@ export async function listBlueprints(
|
|||||||
|
|
||||||
return response<ListBlueprintsResponse>(res, {
|
return response<ListBlueprintsResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
blueprints: blueprintsList.map((b) => ({
|
blueprints:
|
||||||
...b,
|
blueprintsList as ListBlueprintsResponse["blueprints"],
|
||||||
createdAt: new Date(b.createdAt * 1000)
|
|
||||||
})) as BlueprintData[],
|
|
||||||
pagination: {
|
pagination: {
|
||||||
total: count,
|
total: count,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { Blueprint } from "@server/db";
|
|||||||
|
|
||||||
export type BlueprintSource = "API" | "UI" | "NEWT";
|
export type BlueprintSource = "API" | "UI" | "NEWT";
|
||||||
|
|
||||||
export type BlueprintData = Omit<Blueprint, "source" | "createdAt"> & {
|
export type BlueprintData = Omit<Blueprint, "source"> & {
|
||||||
source: BlueprintSource;
|
source: BlueprintSource;
|
||||||
createdAt: Date;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -826,6 +826,13 @@ authenticated.put(
|
|||||||
blueprints.createAndApplyBlueprint
|
blueprints.createAndApplyBlueprint
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/blueprint/:blueprintId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.getBlueprint),
|
||||||
|
blueprints.getBlueprint
|
||||||
|
);
|
||||||
|
|
||||||
// Auth routes
|
// Auth routes
|
||||||
export const authRouter = Router();
|
export const authRouter = Router();
|
||||||
unauthenticated.use("/auth", authRouter);
|
unauthenticated.use("/auth", authRouter);
|
||||||
|
|||||||
66
src/app/[orgId]/settings/blueprints/[blueprintId]/page.tsx
Normal file
66
src/app/[orgId]/settings/blueprints/[blueprintId]/page.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import BlueprintDetailsForm from "@app/components/BlueprintDetailsForm";
|
||||||
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { internal } from "@app/lib/api";
|
||||||
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
|
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
|
||||||
|
import { GetBlueprintResponse } from "@server/routers/blueprints";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
|
||||||
|
type BluePrintsPageProps = {
|
||||||
|
params: Promise<{ orgId: string; blueprintId: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Blueprint Detail"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function BluePrintDetailPage(props: BluePrintsPageProps) {
|
||||||
|
const params = await props.params;
|
||||||
|
let org = null;
|
||||||
|
try {
|
||||||
|
const res = await getCachedOrg(params.orgId);
|
||||||
|
org = res.data.data;
|
||||||
|
} catch {
|
||||||
|
redirect(`/${params.orgId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let blueprint = null;
|
||||||
|
try {
|
||||||
|
const res = await internal.get<AxiosResponse<GetBlueprintResponse>>(
|
||||||
|
`/org/${params.orgId}/blueprint/${params.blueprintId}`,
|
||||||
|
await authCookieHeader()
|
||||||
|
);
|
||||||
|
|
||||||
|
blueprint = res.data.data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = await getTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-2 items-start">
|
||||||
|
<Button variant="link" asChild className="gap-1 px-0">
|
||||||
|
<Link href={`/${params.orgId}/settings/blueprints`}>
|
||||||
|
<ArrowLeft className="size-4 flex-none" />
|
||||||
|
{t("blueprintGoBack")}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<SettingsSectionTitle
|
||||||
|
title={t("blueprintDetails")}
|
||||||
|
description={t("blueprintDetailsDescription")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BlueprintDetailsForm blueprint={blueprint} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,18 +4,16 @@ import BlueprintsTable, {
|
|||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { internal } from "@app/lib/api";
|
import { internal } from "@app/lib/api";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
|
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
|
||||||
import OrgProvider from "@app/providers/OrgProvider";
|
import OrgProvider from "@app/providers/OrgProvider";
|
||||||
import { ListBlueprintsResponse } from "@server/routers/blueprints";
|
import { ListBlueprintsResponse } from "@server/routers/blueprints";
|
||||||
import { GetOrgResponse } from "@server/routers/org";
|
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { cache } from "react";
|
|
||||||
|
|
||||||
type BluePrintsPageProps = {
|
type BluePrintsPageProps = {
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
searchParams: Promise<{ view?: string }>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -39,13 +37,7 @@ export default async function BluePrintsPage(props: BluePrintsPageProps) {
|
|||||||
|
|
||||||
let org = null;
|
let org = null;
|
||||||
try {
|
try {
|
||||||
const getOrg = cache(async () =>
|
const res = await getCachedOrg(params.orgId);
|
||||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
|
||||||
`/org/${params.orgId}`,
|
|
||||||
await authCookieHeader()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const res = await getOrg();
|
|
||||||
org = res.data.data;
|
org = res.data.data;
|
||||||
} catch {
|
} catch {
|
||||||
redirect(`/${params.orgId}`);
|
redirect(`/${params.orgId}`);
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { GetOrgResponse } from "@server/routers/org";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import OrgProvider from "@app/providers/OrgProvider";
|
import OrgProvider from "@app/providers/OrgProvider";
|
||||||
import { ListDomainsResponse } from "@server/routers/domain";
|
import { ListDomainsResponse } from "@server/routers/domain";
|
||||||
import { toUnicode } from 'punycode';
|
import { toUnicode } from "punycode";
|
||||||
|
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
@@ -20,15 +21,16 @@ export default async function DomainsPage(props: Props) {
|
|||||||
|
|
||||||
let domains: DomainRow[] = [];
|
let domains: DomainRow[] = [];
|
||||||
try {
|
try {
|
||||||
const res = await internal.get<
|
const res = await internal.get<AxiosResponse<ListDomainsResponse>>(
|
||||||
AxiosResponse<ListDomainsResponse>
|
`/org/${params.orgId}/domains`,
|
||||||
>(`/org/${params.orgId}/domains`, await authCookieHeader());
|
await authCookieHeader()
|
||||||
|
);
|
||||||
|
|
||||||
const rawDomains = res.data.data.domains as DomainRow[];
|
const rawDomains = res.data.data.domains as DomainRow[];
|
||||||
|
|
||||||
domains = rawDomains.map((domain) => ({
|
domains = rawDomains.map((domain) => ({
|
||||||
...domain,
|
...domain,
|
||||||
baseDomain: toUnicode(domain.baseDomain),
|
baseDomain: toUnicode(domain.baseDomain)
|
||||||
}));
|
}));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -36,21 +38,12 @@ export default async function DomainsPage(props: Props) {
|
|||||||
|
|
||||||
let org = null;
|
let org = null;
|
||||||
try {
|
try {
|
||||||
const getOrg = cache(async () =>
|
const res = await getCachedOrg(params.orgId);
|
||||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
|
||||||
`/org/${params.orgId}`,
|
|
||||||
await authCookieHeader()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const res = await getOrg();
|
|
||||||
org = res.data.data;
|
org = res.data.data;
|
||||||
} catch {
|
} catch {
|
||||||
redirect(`/${params.orgId}`);
|
redirect(`/${params.orgId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!org) {
|
|
||||||
}
|
|
||||||
|
|
||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
17
src/app/[orgId]/settings/not-found.tsx
Normal file
17
src/app/[orgId]/settings/not-found.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
|
export default async function NotFound() {
|
||||||
|
const t = await getTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-md mx-auto p-3 md:mt-32 text-center">
|
||||||
|
<h1 className="text-6xl font-bold mb-4">404</h1>
|
||||||
|
<h2 className="text-2xl font-semibold text-neutral-500 mb-4">
|
||||||
|
{t("pageNotFound")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-neutral-500 dark:text-neutral-700 mb-8">
|
||||||
|
{t("pageNotFoundDescription")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
211
src/components/BlueprintDetailsForm.tsx
Normal file
211
src/components/BlueprintDetailsForm.tsx
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
SettingsContainer,
|
||||||
|
SettingsSection,
|
||||||
|
SettingsSectionBody,
|
||||||
|
SettingsSectionForm,
|
||||||
|
SettingsSectionHeader,
|
||||||
|
SettingsSectionTitle
|
||||||
|
} from "@app/components/Settings";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage
|
||||||
|
} from "@app/components/ui/form";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { Input } from "./ui/input";
|
||||||
|
import Editor from "@monaco-editor/react";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import type { GetBlueprintResponse } from "@server/routers/blueprints";
|
||||||
|
import { Alert, AlertDescription } from "./ui/alert";
|
||||||
|
import {
|
||||||
|
InfoSection,
|
||||||
|
InfoSectionContent,
|
||||||
|
InfoSections,
|
||||||
|
InfoSectionTitle
|
||||||
|
} from "./InfoSection";
|
||||||
|
import { Badge } from "./ui/badge";
|
||||||
|
import { Globe, Terminal, Webhook } from "lucide-react";
|
||||||
|
|
||||||
|
export type CreateBlueprintFormProps = {
|
||||||
|
blueprint: GetBlueprintResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BlueprintDetailsForm({
|
||||||
|
blueprint
|
||||||
|
}: CreateBlueprintFormProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
disabled: true,
|
||||||
|
defaultValues: {
|
||||||
|
name: blueprint.name,
|
||||||
|
contents: blueprint.contents
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
<InfoSections cols={2}>
|
||||||
|
<InfoSection>
|
||||||
|
<InfoSectionTitle>
|
||||||
|
{t("appliedAt")}
|
||||||
|
</InfoSectionTitle>
|
||||||
|
<InfoSectionContent>
|
||||||
|
<time
|
||||||
|
className="text-muted-foreground"
|
||||||
|
dateTime={blueprint.createdAt.toString()}
|
||||||
|
>
|
||||||
|
{new Date(
|
||||||
|
blueprint.createdAt * 1000
|
||||||
|
).toLocaleString()}
|
||||||
|
</time>
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
<InfoSection>
|
||||||
|
<InfoSectionTitle>
|
||||||
|
{t("status")}
|
||||||
|
</InfoSectionTitle>
|
||||||
|
<InfoSectionContent>
|
||||||
|
{blueprint.succeeded ? (
|
||||||
|
<Badge variant="green">
|
||||||
|
{t("success")}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="red">
|
||||||
|
{t("failed", {
|
||||||
|
fallback: "Failed"
|
||||||
|
})}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
<InfoSection>
|
||||||
|
<InfoSectionTitle>
|
||||||
|
{t("message")}
|
||||||
|
</InfoSectionTitle>
|
||||||
|
<InfoSectionContent>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{blueprint.message}
|
||||||
|
</p>
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
<InfoSection>
|
||||||
|
<InfoSectionTitle>
|
||||||
|
{t("source")}
|
||||||
|
</InfoSectionTitle>
|
||||||
|
<InfoSectionContent>
|
||||||
|
{blueprint.source === "API" && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="-mx-2"
|
||||||
|
>
|
||||||
|
<span className="inline-flex items-center gap-1 ">
|
||||||
|
API
|
||||||
|
<Webhook className="size-4 flex-none" />
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{blueprint.source === "NEWT" && (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
<span className="inline-flex items-center gap-1 ">
|
||||||
|
Newt CLI
|
||||||
|
<Terminal className="size-4 flex-none" />
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{blueprint.source === "UI" && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="-mx-1 rounded-sm py-1"
|
||||||
|
>
|
||||||
|
<span className="inline-flex items-center gap-1 ">
|
||||||
|
Dashboard{" "}
|
||||||
|
<Globe className="size-4 flex-none" />
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
)}{" "}
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
</InfoSections>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<SettingsContainer>
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
{t("blueprintInfo")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<SettingsSectionForm className="max-w-2xl">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("name")}</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t("blueprintNameDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="contents"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("contents")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"blueprintContentsDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"resize-y h-64 min-h-64 overflow-y-auto overflow-x-clip max-w-full rounded-md"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
className="w-full h-full max-w-full"
|
||||||
|
language="yaml"
|
||||||
|
theme="vs-dark"
|
||||||
|
options={{
|
||||||
|
minimap: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
readOnly: true
|
||||||
|
}}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SettingsSectionForm>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
</SettingsContainer>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,35 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import { DomainsDataTable } from "@app/components/DomainsDataTable";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
Globe,
|
Globe,
|
||||||
LucideIcon,
|
|
||||||
MoreHorizontal,
|
|
||||||
Terminal,
|
Terminal,
|
||||||
Webhook
|
Webhook
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState, useTransition } from "react";
|
import { useTransition } from "react";
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
|
||||||
import { formatAxiosError } from "@app/lib/api";
|
|
||||||
import { createApiClient } from "@app/lib/api";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
||||||
import { Badge } from "@app/components/ui/badge";
|
import { Badge } from "@app/components/ui/badge";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import CreateDomainForm from "@app/components/CreateDomainForm";
|
|
||||||
import { useToast } from "@app/hooks/useToast";
|
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
|
||||||
import { DataTable } from "./ui/data-table";
|
import { DataTable } from "./ui/data-table";
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger
|
|
||||||
} from "./ui/dropdown-menu";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ListBlueprintsResponse } from "@server/routers/blueprints";
|
import { ListBlueprintsResponse } from "@server/routers/blueprints";
|
||||||
|
|
||||||
@@ -68,7 +52,9 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
|||||||
className="text-muted-foreground"
|
className="text-muted-foreground"
|
||||||
dateTime={row.original.createdAt.toString()}
|
dateTime={row.original.createdAt.toString()}
|
||||||
>
|
>
|
||||||
{new Date(row.original.createdAt).toLocaleString()}
|
{new Date(
|
||||||
|
row.original.createdAt * 1000
|
||||||
|
).toLocaleString()}
|
||||||
</time>
|
</time>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -179,11 +165,11 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const domain = row.original;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button variant="outline" className="items-center" asChild>
|
<Button variant="outline" className="items-center" asChild>
|
||||||
<Link href={`#`}>
|
<Link
|
||||||
|
href={`/${orgId}/settings/blueprints/${row.original.blueprintId}`}
|
||||||
|
>
|
||||||
View details{" "}
|
View details{" "}
|
||||||
<ArrowRight className="size-4 flex-none" />
|
<ArrowRight className="size-4 flex-none" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
SettingsContainer,
|
SettingsContainer,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
SettingsSectionBody,
|
SettingsSectionBody,
|
||||||
SettingsSectionDescription,
|
|
||||||
SettingsSectionForm,
|
SettingsSectionForm,
|
||||||
SettingsSectionHeader,
|
SettingsSectionHeader,
|
||||||
SettingsSectionTitle
|
SettingsSectionTitle
|
||||||
@@ -22,11 +21,10 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { useActionState, useTransition } from "react";
|
import { useActionState } from "react";
|
||||||
import Editor, { useMonaco } from "@monaco-editor/react";
|
import Editor from "@monaco-editor/react";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { wait } from "@app/lib/wait";
|
|
||||||
import { parse as parseYaml } from "yaml";
|
import { parse as parseYaml } from "yaml";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
|
||||||
export function InfoSections({
|
export function InfoSections({
|
||||||
children,
|
children,
|
||||||
cols
|
cols
|
||||||
@@ -9,25 +11,44 @@ export function InfoSections({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`grid md:grid-cols-${cols || 1} md:gap-4 gap-2 md:items-start grid-cols-1`}
|
className={`grid md:grid-cols-[var(--columns)] md:gap-4 gap-2 md:items-start grid-cols-1`}
|
||||||
|
style={{
|
||||||
|
// @ts-expect-error dynamic props don't work with tailwind, but we can set the
|
||||||
|
// value of a CSS variable at runtime and tailwind will just reuse that value
|
||||||
|
"--columns": `repeat(${cols || 1}, minmax(0, 1fr))`
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InfoSection({ children }: { children: React.ReactNode }) {
|
export function InfoSection({
|
||||||
return <div className="space-y-1">{children}</div>;
|
children,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return <div className={cn("space-y-1", className)}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InfoSectionTitle({ children }: { children: React.ReactNode }) {
|
export function InfoSectionTitle({
|
||||||
return <div className="font-semibold">{children}</div>;
|
children,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return <div className={cn("font-semibold", className)}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InfoSectionContent({
|
export function InfoSectionContent({
|
||||||
children
|
children,
|
||||||
|
className
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return <div className="break-words">{children}</div>;
|
return <div className={cn("break-words", className)}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
InfoSectionTitle
|
InfoSectionTitle
|
||||||
} from "@app/components/InfoSection";
|
} from "@app/components/InfoSection";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { build } from "@server/build";
|
|
||||||
import CertificateStatus from "@app/components/private/CertificateStatus";
|
import CertificateStatus from "@app/components/private/CertificateStatus";
|
||||||
import { toUnicode } from "punycode";
|
import { toUnicode } from "punycode";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
|||||||
12
src/lib/api/getCachedOrg.ts
Normal file
12
src/lib/api/getCachedOrg.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { GetOrgResponse } from "@server/routers/org";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
import { cache } from "react";
|
||||||
|
import { authCookieHeader } from "./cookies";
|
||||||
|
import { internal } from ".";
|
||||||
|
|
||||||
|
export const getCachedOrg = cache(async (orgId: string) =>
|
||||||
|
internal.get<AxiosResponse<GetOrgResponse>>(
|
||||||
|
`/org/${orgId}`,
|
||||||
|
await authCookieHeader()
|
||||||
|
)
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user