This commit is contained in:
Benjamin Toby 2026-03-09 16:49:49 +00:00
parent 8491c52639
commit 76bf623116
27 changed files with 460 additions and 46 deletions

View File

@ -12,7 +12,7 @@ type Props = {
export default function AdminHero({ title, ctas, description }: Props) { export default function AdminHero({ title, ctas, description }: Props) {
return ( return (
<Row className="w-full p-10 justify-between"> <Row className="w-full grid-cell-content justify-between">
<Stack className="gap-2"> <Stack className="gap-2">
<H1 className="admin-h1">{title}</H1> <H1 className="admin-h1">{title}</H1>
{description ? ( {description ? (

View File

@ -1,11 +1,32 @@
import { AppContext } from "@/src/pages/_app"; import { AppContext } from "@/src/pages/_app";
import Row from "@/twui/components/layout/Row"; import Row from "@/twui/components/layout/Row";
import Span from "@/twui/components/layout/Span";
import Stack from "@/twui/components/layout/Stack";
import { useContext } from "react"; import { useContext } from "react";
export default function AdminSummary() { export default function AdminSummary() {
const { pageProps } = useContext(AppContext); const { pageProps } = useContext(AppContext);
const { user, deployment, deployment_id } = pageProps;
console.log("pageProps", pageProps); if (!deployment) return null;
return <Row></Row>; return (
<Stack className="w-full gap-0">
<Stack className="grid-cell-content">
<Span>{deployment.services.length} Services</Span>
<Row>
{deployment.services.map((service, index) => {
return (
<a
key={index}
href={`/admin/services/${service.service_name}`}
>
<code>{service.service_name}</code>
</a>
);
})}
</Row>
</Stack>
</Stack>
);
} }

View File

@ -1,18 +1,31 @@
import Stack from "@/twui/components/layout/Stack"; import Stack from "@/twui/components/layout/Stack";
import AdminSummary from "./(sections)/summary"; import AdminSummary from "./(sections)/summary";
import AdminHero from "../../general/admin/hero"; import AdminHero from "../../general/admin/hero";
import { useContext } from "react";
import { AppContext } from "@/src/pages/_app";
import twuiSlugToNormalText from "@/twui/components/utils/slug-to-normal-text";
import Divider from "@/twui/components/layout/Divider";
export default function Main() { export default function Main() {
const { pageProps } = useContext(AppContext);
const { user, deployment, deployment_id } = pageProps;
const deployment_name = deployment?.deployment_name;
return ( return (
<Stack> <Stack>
<AdminHero <AdminHero
title={`Dashboard`} title={`${twuiSlugToNormalText(deployment_name)} Deplyoment Dashboard`}
description={ description={
<> <>
Deployment <code>ad9asd</code> Deployment{" "}
<code>{deployment_id?.split("-").shift()}</code>
{` > `}
<code>{deployment?.deployment_name}</code>
</> </>
} }
/> />
<Divider />
<AdminSummary /> <AdminSummary />
</Stack> </Stack>
); );

View File

@ -0,0 +1,30 @@
import Stack from "@/twui/components/layout/Stack";
import { useContext } from "react";
import { AppContext } from "@/src/pages/_app";
import twuiSlugToNormalText from "@/twui/components/utils/slug-to-normal-text";
import Divider from "@/twui/components/layout/Divider";
import AdminHero from "@/src/components/general/admin/hero";
export default function Main() {
const { pageProps } = useContext(AppContext);
const { user, deployment, deployment_id } = pageProps;
const deployment_name = deployment?.deployment_name;
return (
<Stack>
<AdminHero
title={`${twuiSlugToNormalText(deployment_name)} Deplyoment Dashboard`}
description={
<>
Deployment{" "}
<code>{deployment_id?.split("-").shift()}</code>
{` > `}
<code>{deployment?.deployment_name}</code>
</>
}
/>
<Divider />
</Stack>
);
}

View File

@ -0,0 +1,30 @@
import Stack from "@/twui/components/layout/Stack";
import { useContext } from "react";
import { AppContext } from "@/src/pages/_app";
import twuiSlugToNormalText from "@/twui/components/utils/slug-to-normal-text";
import Divider from "@/twui/components/layout/Divider";
import AdminHero from "@/src/components/general/admin/hero";
export default function Main() {
const { pageProps } = useContext(AppContext);
const { service, deployment } = pageProps;
const deployment_name = deployment?.deployment_name;
const service_name = service?.service_name;
return (
<Stack>
<AdminHero
title={`${twuiSlugToNormalText(service_name)} Service`}
description={
<>
Deployment <code>{deployment_name}</code>
{` > `}
<code>{service_name}</code>
</>
}
/>
<Divider />
</Stack>
);
}

View File

@ -1,8 +1,10 @@
import { EJSON } from "@/src/exports/client-exports"; import { EJSON } from "@/src/exports/client-exports";
import { PagePropsType, URLQueryType, User } from "@/src/types"; import { PagePropsType, URLQueryType, User } from "@/src/types";
import grabDirNames from "@/src/utils/grab-dir-names";
import grabTurboCiConfig from "@/src/utils/grab-turboci-config"; import grabTurboCiConfig from "@/src/utils/grab-turboci-config";
import parsePageUrl from "@/src/utils/parse-page-url"; import parsePageUrl from "@/src/utils/parse-page-url";
import userAuth from "@/src/utils/user-auth"; import userAuth from "@/src/utils/user-auth";
import { readFileSync } from "fs";
import _ from "lodash"; import _ from "lodash";
import { GetServerSidePropsContext, GetServerSidePropsResult } from "next"; import { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
@ -20,6 +22,8 @@ type Params = {
) => Promise<PagePropsType | false | string>; ) => Promise<PagePropsType | false | string>;
}; };
const { TURBOCI_DEPLOYMENT_ID_FILE } = grabDirNames();
export default async function defaultAdminProps({ export default async function defaultAdminProps({
ctx, ctx,
props, props,
@ -29,7 +33,29 @@ export default async function defaultAdminProps({
const query: URLQueryType = ctx.query; const query: URLQueryType = ctx.query;
const { singleRes: user } = await userAuth({ req }); const { singleRes: user } = await userAuth({ req });
const config = grabTurboCiConfig(); const deployment = grabTurboCiConfig();
const deployment_id = readFileSync(TURBOCI_DEPLOYMENT_ID_FILE, "utf-8");
const service = query.service_name
? deployment.services.find(
(srv) => srv.service_name == query.service_name,
) || null
: null;
const children_services = service?.service_name
? deployment.services.filter(
(srv) => srv.parent_service_name == service.service_name,
) || null
: null;
if (query.service_name && !service?.service_name) {
return {
redirect: {
destination: `/admin/services`,
statusCode: 307,
},
};
}
if (!user?.id) { if (!user?.id) {
return { return {
@ -68,7 +94,10 @@ export default async function defaultAdminProps({
query, query,
user, user,
pageUrl: finalAdminUrl, pageUrl: finalAdminUrl,
config, deployment,
deployment_id,
service,
children_services,
}; };
let finalProps = _.merge(props, propsFnProps, defaultPageProps); let finalProps = _.merge(props, propsFnProps, defaultPageProps);

43
src/hooks/use-app-init.ts Normal file
View File

@ -0,0 +1,43 @@
import React, { useEffect } from "react";
import { PagePropsType, ToastType, WebSocketDataType } from "../types";
import useWebSocket from "@/twui/components/hooks/useWebSocket";
import useStatus from "@/twui/components/hooks/useStatus";
export default function useAppInit(pageProps: PagePropsType) {
const wsURL = process.env.NEXT_PUBLIC_WEBSOCKET_URL || "";
const { user } = pageProps;
const [toast, setToast] = React.useState<ToastType>({
toastOpen: false,
});
const { socket, sendData } = useWebSocket<WebSocketDataType>({
url: wsURL,
disableReconnect: false,
keepAliveDuration: 5000,
});
const { loading, setLoading, refresh, setRefresh } = useStatus();
const ws = { socket, sendData };
useEffect(() => {
console.log("wsURL", wsURL);
console.log("socket", socket);
}, [socket]);
return {
socket,
sendData,
loading,
setLoading,
refresh,
setRefresh,
ws,
user,
pageProps,
toast,
setToast,
};
}

View File

@ -11,7 +11,7 @@ export const AdminAsideLinks: (
strict: true, strict: true,
}, },
{ {
title: "Deployments", title: "Services",
url: "/admin/deployments", url: "/admin/services",
}, },
]; ];

View File

@ -8,14 +8,14 @@ export default function Header({ children }: Props) {
return ( return (
<header className="col-span-6"> <header className="col-span-6">
<Row className="w-full grid grid-cols-6 grid-frame nested-grid-frame"> <Row className="w-full grid grid-cols-6 grid-frame nested-grid-frame">
<Row className="h-full items-stretch grid-cell col-span-1 w-full justify-between"> <Row className="h-full items-stretch grid-cell col-span-3 xl:col-span-1 w-full justify-between">
<Row className="px-4"> <Row className="px-4">
<Logo /> <Logo />
</Row> </Row>
{/* <Divider vertical className="-mr-[7px]" /> */} {/* <Divider vertical className="-mr-[7px]" /> */}
</Row> </Row>
<Row className="grid-cell col-span-4"></Row> <Row className="grid-cell col-span-4 hidden xl:block"></Row>
<Row className="grid-cell col-span-1"></Row> <Row className="grid-cell col-span-3 xl:col-span-1"></Row>
</Row> </Row>
</header> </header>
); );

View File

@ -11,18 +11,20 @@ type Props = PropsWithChildren & {};
export default function Layout({ children }: Props) { export default function Layout({ children }: Props) {
return ( return (
<Main className="w-screen h-screen overflow-hidden p-4 lg:p-10"> <Main className="w-screen h-screen overflow-hidden p-4 lg:p-10">
<div className="grid-frame grid-cols-6 w-full h-full grid-rows-[64px_1fr]"> <div className="grid-frame grid-cols-6 w-full h-full grid-rows-[64px_47px] xl:grid-rows-[64px_auto]">
<Header /> <Header />
<Stack className="grid-cell col-span-1"> <Stack className="grid-cell col-span-6 xl:col-span-1">
<LinkList <LinkList
links={AdminAsideLinks} links={AdminAsideLinks}
className="w-full flex-col" className="w-full xl:flex-col"
linkProps={{ linkProps={{
className: "turboci-admin-aside-link", className: "turboci-admin-aside-link",
}} }}
/> />
</Stack> </Stack>
<Stack className="grid-cell col-span-5">{children}</Stack> <Stack className="grid-cell col-span-6 xl:col-span-5">
{children}
</Stack>
</div> </div>
</Main> </Main>
); );

View File

@ -2,14 +2,17 @@ import "@/src/styles/globals.css";
import type { AppProps } from "next/app"; import type { AppProps } from "next/app";
import { createContext } from "react"; import { createContext } from "react";
import { PagePropsType, TurboCIAdminAppContextType } from "../types"; import { PagePropsType, TurboCIAdminAppContextType } from "../types";
import useAppInit from "../hooks/use-app-init";
export const AppContext = createContext<TurboCIAdminAppContextType>( export const AppContext = createContext<TurboCIAdminAppContextType>(
{} as TurboCIAdminAppContextType, {} as TurboCIAdminAppContextType,
); );
export default function App({ Component, pageProps }: AppProps<PagePropsType>) { export default function App({ Component, pageProps }: AppProps<PagePropsType>) {
const init = useAppInit(pageProps);
return ( return (
<AppContext.Provider value={{ pageProps }}> <AppContext.Provider value={{ ...init }}>
<Component {...pageProps} /> <Component {...pageProps} />
</AppContext.Provider> </AppContext.Provider>
); );

View File

@ -0,0 +1,18 @@
import Main from "@/src/components/pages/admin/services/service";
import defaultAdminProps from "@/src/functions/pages/admin/default-admin-props";
import Layout from "@/src/layouts/admin";
import { GetServerSideProps } from "next";
export default function AdminSingleService() {
return (
<Layout>
<Main />
</Layout>
);
}
export const getServerSideProps: GetServerSideProps = async (ctx) => {
return await defaultAdminProps({
ctx,
});
};

View File

@ -0,0 +1,18 @@
import Main from "@/src/components/pages/admin/services";
import defaultAdminProps from "@/src/functions/pages/admin/default-admin-props";
import Layout from "@/src/layouts/admin";
import { GetServerSideProps } from "next";
export default function AdminServices() {
return (
<Layout>
<Main />
</Layout>
);
}
export const getServerSideProps: GetServerSideProps = async (ctx) => {
return await defaultAdminProps({
ctx,
});
};

View File

@ -29,9 +29,8 @@
@apply gap-px p-px; @apply gap-px p-px;
} }
.grid-frame.nested-grid-frame { .nested-grid-frame {
@apply grid bg-transparent! dark:bg-transparent!; @apply bg-foreground-light/10 dark:bg-foreground-dark/10 grid p-0! h-full;
@apply gap-px p-0! h-full;
} }
.grid-frame.nested-grid-frame .grid-cell { .grid-frame.nested-grid-frame .grid-cell {
@ -43,7 +42,7 @@
} }
.grid-cell-content { .grid-cell-content {
@apply p-10; @apply p-4 xl:p-10;
} }
.twui-button, .twui-button,
@ -74,7 +73,7 @@
} }
.turboci-admin-aside-link { .turboci-admin-aside-link {
@apply w-full border-foreground-light/10 dark:border-r-foreground-dark/10 py-4 px-6; @apply w-full border-b-0 xl:border-foreground-light/10 xl:dark:border-foreground-dark/10 py-4 px-6;
@apply text-foreground-light; @apply text-foreground-light;
} }

View File

@ -1,4 +1,6 @@
import { ToastStyles } from "@/twui/components/elements/Toast";
import { DATASQUIREL_LoggedInUser } from "@moduletrace/datasquirel/dist/package-shared/types"; import { DATASQUIREL_LoggedInUser } from "@moduletrace/datasquirel/dist/package-shared/types";
import useAppInit from "../hooks/use-app-init";
export type User = DATASQUIREL_LoggedInUser & {}; export type User = DATASQUIREL_LoggedInUser & {};
@ -169,13 +171,18 @@ export type ServiceScriptObject = {
work_dir?: string; work_dir?: string;
}; };
export type URLQueryType = {}; export type URLQueryType = {
service_name?: string | null;
};
export type PagePropsType = { export type PagePropsType = {
config?: TCIGlobalConfig | null; deployment?: TCIGlobalConfig | null;
query?: URLQueryType | null; query?: URLQueryType | null;
user: User; user: User;
pageUrl?: string | null; pageUrl?: string | null;
deployment_id?: string | null;
service?: ParsedDeploymentServiceConfig | null;
children_services?: ParsedDeploymentServiceConfig[] | null;
}; };
export type APIReqObject = { export type APIReqObject = {
@ -208,6 +215,34 @@ export type TurboCISignupFormObject = {
confirmed_password?: string; confirmed_password?: string;
}; };
export type TurboCIAdminAppContextType = { export type TurboCIAdminAppContextType = ReturnType<typeof useAppInit>;
pageProps: PagePropsType;
export const WebSocketEvents = [
"client:ping",
"server:ping",
"server:error",
"server:message",
"server:ready",
"server:success",
"server:update",
] as const;
export type WebSocketDataType = {
event: (typeof WebSocketEvents)[number];
message?: string;
};
export type WebSocketType = {
socket?: WebSocket;
data?: WebSocketDataType | null;
message?: string;
sendData?: (data: WebSocketDataType) => void;
};
export type ToastType = {
toastStyle?: (typeof ToastStyles)[number];
toastMessage?: string;
toastOpen: boolean;
closeDelay?: number;
}; };

View File

@ -49,14 +49,8 @@ export function setCookie(
res.setHeader("Set-Cookie", final_cookie_string); res.setHeader("Set-Cookie", final_cookie_string);
} }
export function getCookie( export function getCookie(cookie_string: string, name: string): string | null {
req: http.IncomingMessage, const cookies = cookie_string
name: string,
): string | null {
const cookieHeader = req.headers.cookie;
if (!cookieHeader) return null;
const cookies = cookieHeader
.split(";") .split(";")
.reduce((acc: { [key: string]: string }, cookie: string) => { .reduce((acc: { [key: string]: string }, cookie: string) => {
const [key, val] = cookie.trim().split("=").map(decodeURIComponent); const [key, val] = cookie.trim().split("=").map(decodeURIComponent);

View File

@ -8,17 +8,28 @@ import { EJSON } from "../exports/client-exports";
import grabCookieNames from "./grab-cookie-names"; import grabCookieNames from "./grab-cookie-names";
type Params = { type Params = {
req: req?:
| NextApiRequest | NextApiRequest
| (IncomingMessage & { cookies: Partial<{ [key: string]: string }> }); | (IncomingMessage & { cookies: Partial<{ [key: string]: string }> });
bun_req?: Request;
}; };
export default async function userAuth({ export default async function userAuth({
req, req,
bun_req,
}: Params): Promise<APIResponseObject<User>> { }: Params): Promise<APIResponseObject<User>> {
try { try {
const { auth_key_cookie_name, csrf_cookie_name } = grabCookieNames(); const { auth_key_cookie_name, csrf_cookie_name } = grabCookieNames();
const key = getCookie(req, auth_key_cookie_name); const cookie_string =
req?.headers.cookie || bun_req?.headers.get("cookie");
if (!cookie_string) {
return {
success: false,
msg: `Couldn't grab cookie string`,
};
}
const key = getCookie(cookie_string, auth_key_cookie_name);
if (!key) { if (!key) {
return { return {
@ -37,7 +48,7 @@ export default async function userAuth({
}; };
} }
const csrf = getCookie(req, csrf_cookie_name); const csrf = getCookie(cookie_string, csrf_cookie_name);
if (!csrf) { if (!csrf) {
return { return {

View File

@ -0,0 +1,11 @@
import type { ServerWebSocket } from "bun";
import datasquirel from "@moduletrace/datasquirel";
import { WebSocketData, WebSocketDataType } from "@/src/types";
const EJSON = datasquirel.client.utils.EJSON;
export default function sendData(
ws: ServerWebSocket<WebSocketData>,
data: WebSocketDataType,
) {
ws.send(String(EJSON.stringify(data)));
}

View File

@ -0,0 +1,18 @@
import type { ServerWebSocket } from "bun";
import { WebSocketData, WebSocketDataType } from "@/src/types";
import datasquirel from "@moduletrace/datasquirel";
const EJSON = datasquirel.client.utils.EJSON;
export default function sendError(
ws: ServerWebSocket<WebSocketData>,
message?: String,
) {
ws.send(
String(
EJSON.stringify({
event: "server:error",
message: message,
} as WebSocketDataType),
),
);
}

View File

@ -0,0 +1,18 @@
import type { ServerWebSocket } from "bun";
import { WebSocketData, WebSocketDataType } from "@/src/types";
import datasquirel from "@moduletrace/datasquirel";
const EJSON = datasquirel.client.utils.EJSON;
export default function sendMessage(
ws: ServerWebSocket<WebSocketData>,
message: String,
) {
ws.send(
String(
EJSON.stringify({
event: "server:message",
message: message,
} as WebSocketDataType),
),
);
}

View File

@ -0,0 +1,18 @@
import type { ServerWebSocket } from "bun";
import { WebSocketData, WebSocketDataType } from "@/src/types";
import datasquirel from "@moduletrace/datasquirel";
const EJSON = datasquirel.client.utils.EJSON;
export default function sendReady(
ws: ServerWebSocket<WebSocketData>,
message?: String,
) {
ws.send(
String(
EJSON.stringify({
event: "server:ready",
message: message,
} as WebSocketDataType),
),
);
}

View File

@ -0,0 +1,18 @@
import type { ServerWebSocket } from "bun";
import { WebSocketData, WebSocketDataType } from "@/src/types";
import datasquirel from "@moduletrace/datasquirel";
const EJSON = datasquirel.client.utils.EJSON;
export default function sendSuccess(
ws: ServerWebSocket<WebSocketData>,
message: String,
) {
ws.send(
String(
EJSON.stringify({
event: "server:success",
message: message,
} as WebSocketDataType),
),
);
}

View File

@ -0,0 +1,18 @@
import type { ServerWebSocket } from "bun";
import { WebSocketData, WebSocketDataType } from "@/src/types";
import datasquirel from "@moduletrace/datasquirel";
const EJSON = datasquirel.client.utils.EJSON;
export default function sendUpdate(
ws: ServerWebSocket<WebSocketData>,
message: String,
) {
ws.send(
String(
EJSON.stringify({
event: "server:update",
message: message,
} as WebSocketDataType),
),
);
}

View File

@ -0,0 +1,16 @@
import sendData from "../(utils)/send-data";
import sendError from "../(utils)/send-error";
import { WebSocketMessageParam } from "../socket-message";
export default async function socketClientPing({
ws,
data,
}: WebSocketMessageParam) {
try {
sendData(ws, {
event: "server:ping",
});
} catch (error: any) {
sendError(ws, "Client Ping Error! " + error.message);
}
}

View File

@ -1,5 +1,6 @@
import { WebSocketData } from "../types"; import { WebSocketData } from "../types";
import socketInit from "./socket-init"; import socketInit from "./socket-init";
import socketMessage from "./socket-message";
const server = Bun.serve<WebSocketData>({ const server = Bun.serve<WebSocketData>({
async fetch(req, server) { async fetch(req, server) {
@ -22,10 +23,17 @@ const server = Bun.serve<WebSocketData>({
websocket: { websocket: {
async message(ws, message) { async message(ws, message) {
if (typeof message == "string") { if (typeof message == "string") {
await socketMessage({ ws, message });
} }
}, },
async open(ws) {}, async open(ws) {
async close(ws, code, message) {}, const user = ws.data.user;
console.log(`Web Socket Opened by ${user.first_name}`);
},
async close(ws, code, message) {
const user = ws.data.user;
console.log(`Socket Closed by ${user.first_name}`);
},
idleTimeout: 600, idleTimeout: 600,
maxPayloadLength: 1024 * 1024 * 10, maxPayloadLength: 1024 * 1024 * 10,
}, },

View File

@ -1,5 +1,6 @@
import datasquirel from "@moduletrace/datasquirel"; import datasquirel from "@moduletrace/datasquirel";
import { User } from "../types"; import { User } from "../types";
import userAuth from "../utils/user-auth";
type Param = { type Param = {
req: Request; req: Request;
@ -7,7 +8,7 @@ type Param = {
}; };
type Return = { type Return = {
user: User | null; user?: User | null;
}; };
export default async function socketInit({ export default async function socketInit({
@ -25,15 +26,11 @@ export default async function socketInit({
user: null, user: null,
}; };
const user = datasquirel.user.auth.auth({ const { singleRes: user } = await userAuth({ bun_req: req });
cookieString,
database: process.env.DSQL_DB_NAME || "",
skipFileCheck: true,
});
if (debug) { if (debug) {
console.log("DEBUG:::socketInit:user", user); console.log("DEBUG:::socketInit:user", user);
} }
return { user: user.payload as User | null }; return { user };
} }

View File

@ -0,0 +1,46 @@
import { ServerWebSocket } from "bun";
import { WebSocketData, WebSocketDataType } from "../types";
import { EJSON } from "../exports/client-exports";
import socketClientPing from "./events/client-ping";
import debugLog from "@moduletrace/datasquirel/dist/package-shared/utils/logging/debug-log";
type Param = {
ws: ServerWebSocket<WebSocketData>;
message: string | Buffer;
};
export type WebSocketMessageParam = {
ws: ServerWebSocket<WebSocketData>;
message?: string | Buffer;
data?: WebSocketDataType;
};
export default async function socketMessage({ ws, message }: Param) {
const user = ws.data.user;
const data = EJSON.parse(message.toString()) as
| WebSocketDataType
| undefined;
const websocketMessageParams: WebSocketMessageParam = {
ws,
data,
message,
};
const userRef = `User ${user.first_name} ${user.last_name} [#${user.id}]`;
const label = "Web Socket Message";
switch (data?.event) {
case "client:ping":
debugLog({
log: `${userRef} Pinging Server ...`,
addTime: true,
label,
});
await socketClientPing(websocketMessageParams);
break;
default:
break;
}
}