mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-03 08:39:09 +00:00
✨ blueprint details page
This commit is contained in:
@@ -1158,7 +1158,10 @@
|
||||
"blueprintGoBack": "Back to blueprints",
|
||||
"blueprintCreate": "Create 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",
|
||||
"message": "Message",
|
||||
"blueprintNameDescription": "This is the display name for the blueprint.",
|
||||
"blueprintContentsDescription": "Define the YAML content describing your infrastructure",
|
||||
"blueprintErrorCreateDescription": "An error occurred when applying the blueprint",
|
||||
|
||||
@@ -119,6 +119,7 @@ export enum ActionsEnum {
|
||||
|
||||
// blueprints
|
||||
listBlueprints = "listBlueprints",
|
||||
getBlueprint = "getBlueprint",
|
||||
applyBlueprint = "applyBlueprint"
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ export const registry = new OpenAPIRegistry();
|
||||
export enum OpenAPITags {
|
||||
Site = "Site",
|
||||
Org = "Organization",
|
||||
Blueprint = "Blueprint",
|
||||
Resource = "Resource",
|
||||
Role = "Role",
|
||||
User = "User",
|
||||
|
||||
@@ -44,7 +44,7 @@ registry.registerPath({
|
||||
path: "/org/{orgId}/blueprint",
|
||||
description:
|
||||
"Create and Apply a base64 encoded blueprint to an organization",
|
||||
tags: [OpenAPITags.Org],
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Blueprint],
|
||||
request: {
|
||||
params: applyBlueprintParamsSchema,
|
||||
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 "./createAndApplyBlueprint";
|
||||
export * from "./getBlueprint";
|
||||
|
||||
@@ -46,6 +46,7 @@ async function queryBlueprints(orgId: string, limit: number, offset: number) {
|
||||
})
|
||||
.from(blueprints)
|
||||
.leftJoin(orgs, eq(blueprints.orgId, orgs.orgId))
|
||||
.where(eq(blueprints.orgId, orgId))
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
return res;
|
||||
@@ -70,7 +71,7 @@ registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/blueprints",
|
||||
description: "List all blueprints for a organization.",
|
||||
tags: [OpenAPITags.Org],
|
||||
tags: [OpenAPITags.Org, OpenAPITags.Blueprint],
|
||||
request: {
|
||||
params: z.object({
|
||||
orgId: z.string()
|
||||
@@ -121,10 +122,8 @@ export async function listBlueprints(
|
||||
|
||||
return response<ListBlueprintsResponse>(res, {
|
||||
data: {
|
||||
blueprints: blueprintsList.map((b) => ({
|
||||
...b,
|
||||
createdAt: new Date(b.createdAt * 1000)
|
||||
})) as BlueprintData[],
|
||||
blueprints:
|
||||
blueprintsList as ListBlueprintsResponse["blueprints"],
|
||||
pagination: {
|
||||
total: count,
|
||||
limit,
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { Blueprint } from "@server/db";
|
||||
|
||||
export type BlueprintSource = "API" | "UI" | "NEWT";
|
||||
|
||||
export type BlueprintData = Omit<Blueprint, "source" | "createdAt"> & {
|
||||
export type BlueprintData = Omit<Blueprint, "source"> & {
|
||||
source: BlueprintSource;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
@@ -826,6 +826,13 @@ authenticated.put(
|
||||
blueprints.createAndApplyBlueprint
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/blueprint/:blueprintId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.getBlueprint),
|
||||
blueprints.getBlueprint
|
||||
);
|
||||
|
||||
// Auth routes
|
||||
export const authRouter = Router();
|
||||
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 { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import { ListBlueprintsResponse } from "@server/routers/blueprints";
|
||||
import { GetOrgResponse } from "@server/routers/org";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { redirect } from "next/navigation";
|
||||
import { cache } from "react";
|
||||
|
||||
type BluePrintsPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
searchParams: Promise<{ view?: string }>;
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -39,13 +37,7 @@ export default async function BluePrintsPage(props: BluePrintsPageProps) {
|
||||
|
||||
let org = null;
|
||||
try {
|
||||
const getOrg = cache(async () =>
|
||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
||||
`/org/${params.orgId}`,
|
||||
await authCookieHeader()
|
||||
)
|
||||
);
|
||||
const res = await getOrg();
|
||||
const res = await getCachedOrg(params.orgId);
|
||||
org = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${params.orgId}`);
|
||||
|
||||
@@ -9,7 +9,8 @@ import { GetOrgResponse } from "@server/routers/org";
|
||||
import { redirect } from "next/navigation";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import { ListDomainsResponse } from "@server/routers/domain";
|
||||
import { toUnicode } from 'punycode';
|
||||
import { toUnicode } from "punycode";
|
||||
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -20,15 +21,16 @@ export default async function DomainsPage(props: Props) {
|
||||
|
||||
let domains: DomainRow[] = [];
|
||||
try {
|
||||
const res = await internal.get<
|
||||
AxiosResponse<ListDomainsResponse>
|
||||
>(`/org/${params.orgId}/domains`, await authCookieHeader());
|
||||
const res = await internal.get<AxiosResponse<ListDomainsResponse>>(
|
||||
`/org/${params.orgId}/domains`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
|
||||
const rawDomains = res.data.data.domains as DomainRow[];
|
||||
|
||||
domains = rawDomains.map((domain) => ({
|
||||
...domain,
|
||||
baseDomain: toUnicode(domain.baseDomain),
|
||||
baseDomain: toUnicode(domain.baseDomain)
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -36,21 +38,12 @@ export default async function DomainsPage(props: Props) {
|
||||
|
||||
let org = null;
|
||||
try {
|
||||
const getOrg = cache(async () =>
|
||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
||||
`/org/${params.orgId}`,
|
||||
await authCookieHeader()
|
||||
)
|
||||
);
|
||||
const res = await getOrg();
|
||||
const res = await getCachedOrg(params.orgId);
|
||||
org = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${params.orgId}`);
|
||||
}
|
||||
|
||||
if (!org) {
|
||||
}
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
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";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DomainsDataTable } from "@app/components/DomainsDataTable";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUpDown,
|
||||
Globe,
|
||||
LucideIcon,
|
||||
MoreHorizontal,
|
||||
Terminal,
|
||||
Webhook
|
||||
} from "lucide-react";
|
||||
import { useState, 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 { useTransition } from "react";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { useRouter } from "next/navigation";
|
||||
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 {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "./ui/dropdown-menu";
|
||||
import Link from "next/link";
|
||||
import { ListBlueprintsResponse } from "@server/routers/blueprints";
|
||||
|
||||
@@ -68,7 +52,9 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
||||
className="text-muted-foreground"
|
||||
dateTime={row.original.createdAt.toString()}
|
||||
>
|
||||
{new Date(row.original.createdAt).toLocaleString()}
|
||||
{new Date(
|
||||
row.original.createdAt * 1000
|
||||
).toLocaleString()}
|
||||
</time>
|
||||
);
|
||||
}
|
||||
@@ -179,11 +165,11 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const domain = row.original;
|
||||
|
||||
return (
|
||||
<Button variant="outline" className="items-center" asChild>
|
||||
<Link href={`#`}>
|
||||
<Link
|
||||
href={`/${orgId}/settings/blueprints/${row.original.blueprintId}`}
|
||||
>
|
||||
View details{" "}
|
||||
<ArrowRight className="size-4 flex-none" />
|
||||
</Link>
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
@@ -22,11 +21,10 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import z from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Input } from "./ui/input";
|
||||
import { useActionState, useTransition } from "react";
|
||||
import Editor, { useMonaco } from "@monaco-editor/react";
|
||||
import { useActionState } from "react";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { Button } from "./ui/button";
|
||||
import { wait } from "@app/lib/wait";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@app/lib/cn";
|
||||
|
||||
export function InfoSections({
|
||||
children,
|
||||
cols
|
||||
@@ -9,25 +11,44 @@ export function InfoSections({
|
||||
}) {
|
||||
return (
|
||||
<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}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function InfoSection({ children }: { children: React.ReactNode }) {
|
||||
return <div className="space-y-1">{children}</div>;
|
||||
export function InfoSection({
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn("space-y-1", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function InfoSectionTitle({ children }: { children: React.ReactNode }) {
|
||||
return <div className="font-semibold">{children}</div>;
|
||||
export function InfoSectionTitle({
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn("font-semibold", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function InfoSectionContent({
|
||||
children
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
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
|
||||
} from "@app/components/InfoSection";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { build } from "@server/build";
|
||||
import CertificateStatus from "@app/components/private/CertificateStatus";
|
||||
import { toUnicode } from "punycode";
|
||||
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