diff --git a/src/components/general/admin/hero.tsx b/src/components/general/admin/hero.tsx index 835100d..07219da 100644 --- a/src/components/general/admin/hero.tsx +++ b/src/components/general/admin/hero.tsx @@ -12,7 +12,7 @@ type Props = { export default function AdminHero({ title, ctas, description }: Props) { return ( - +

{title}

{description ? ( diff --git a/src/components/pages/admin/(sections)/summary.tsx b/src/components/pages/admin/(sections)/summary.tsx index a093fb7..4a96e0e 100644 --- a/src/components/pages/admin/(sections)/summary.tsx +++ b/src/components/pages/admin/(sections)/summary.tsx @@ -1,11 +1,32 @@ import { AppContext } from "@/src/pages/_app"; 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"; export default function AdminSummary() { const { pageProps } = useContext(AppContext); + const { user, deployment, deployment_id } = pageProps; - console.log("pageProps", pageProps); + if (!deployment) return null; - return ; + return ( + + + {deployment.services.length} Services + + {deployment.services.map((service, index) => { + return ( + + {service.service_name} + + ); + })} + + + + ); } diff --git a/src/components/pages/admin/index.tsx b/src/components/pages/admin/index.tsx index 0efdb34..8dd406c 100644 --- a/src/components/pages/admin/index.tsx +++ b/src/components/pages/admin/index.tsx @@ -1,18 +1,31 @@ import Stack from "@/twui/components/layout/Stack"; import AdminSummary from "./(sections)/summary"; 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() { + const { pageProps } = useContext(AppContext); + const { user, deployment, deployment_id } = pageProps; + + const deployment_name = deployment?.deployment_name; + return ( - Deployment ad9asd + Deployment{" "} + {deployment_id?.split("-").shift()} + {` > `} + {deployment?.deployment_name} } /> + ); diff --git a/src/components/pages/admin/services/index.tsx b/src/components/pages/admin/services/index.tsx new file mode 100644 index 0000000..f5ecf3a --- /dev/null +++ b/src/components/pages/admin/services/index.tsx @@ -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 ( + + + Deployment{" "} + {deployment_id?.split("-").shift()} + {` > `} + {deployment?.deployment_name} + + } + /> + + + ); +} diff --git a/src/components/pages/admin/services/service/index.tsx b/src/components/pages/admin/services/service/index.tsx new file mode 100644 index 0000000..49cf2db --- /dev/null +++ b/src/components/pages/admin/services/service/index.tsx @@ -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 ( + + + Deployment {deployment_name} + {` > `} + {service_name} + + } + /> + + + ); +} diff --git a/src/functions/pages/admin/default-admin-props.ts b/src/functions/pages/admin/default-admin-props.ts index 43bf582..ac30895 100644 --- a/src/functions/pages/admin/default-admin-props.ts +++ b/src/functions/pages/admin/default-admin-props.ts @@ -1,8 +1,10 @@ import { EJSON } from "@/src/exports/client-exports"; import { PagePropsType, URLQueryType, User } from "@/src/types"; +import grabDirNames from "@/src/utils/grab-dir-names"; import grabTurboCiConfig from "@/src/utils/grab-turboci-config"; import parsePageUrl from "@/src/utils/parse-page-url"; import userAuth from "@/src/utils/user-auth"; +import { readFileSync } from "fs"; import _ from "lodash"; import { GetServerSidePropsContext, GetServerSidePropsResult } from "next"; @@ -20,6 +22,8 @@ type Params = { ) => Promise; }; +const { TURBOCI_DEPLOYMENT_ID_FILE } = grabDirNames(); + export default async function defaultAdminProps({ ctx, props, @@ -29,7 +33,29 @@ export default async function defaultAdminProps({ const query: URLQueryType = ctx.query; 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) { return { @@ -68,7 +94,10 @@ export default async function defaultAdminProps({ query, user, pageUrl: finalAdminUrl, - config, + deployment, + deployment_id, + service, + children_services, }; let finalProps = _.merge(props, propsFnProps, defaultPageProps); diff --git a/src/hooks/use-app-init.ts b/src/hooks/use-app-init.ts new file mode 100644 index 0000000..8518186 --- /dev/null +++ b/src/hooks/use-app-init.ts @@ -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({ + toastOpen: false, + }); + + const { socket, sendData } = useWebSocket({ + 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, + }; +} diff --git a/src/layouts/admin/(data)/links.ts b/src/layouts/admin/(data)/links.ts index 50d12a3..51dbd69 100644 --- a/src/layouts/admin/(data)/links.ts +++ b/src/layouts/admin/(data)/links.ts @@ -11,7 +11,7 @@ export const AdminAsideLinks: ( strict: true, }, { - title: "Deployments", - url: "/admin/deployments", + title: "Services", + url: "/admin/services", }, ]; diff --git a/src/layouts/admin/header.tsx b/src/layouts/admin/header.tsx index 3b2ef24..32737e2 100644 --- a/src/layouts/admin/header.tsx +++ b/src/layouts/admin/header.tsx @@ -8,14 +8,14 @@ export default function Header({ children }: Props) { return (
- + {/* */} - - + +
); diff --git a/src/layouts/admin/index.tsx b/src/layouts/admin/index.tsx index bc85beb..360a4c9 100644 --- a/src/layouts/admin/index.tsx +++ b/src/layouts/admin/index.tsx @@ -11,18 +11,20 @@ type Props = PropsWithChildren & {}; export default function Layout({ children }: Props) { return (
-
+
- + - {children} + + {children} +
); diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index db8882f..1dd0013 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -2,14 +2,17 @@ import "@/src/styles/globals.css"; import type { AppProps } from "next/app"; import { createContext } from "react"; import { PagePropsType, TurboCIAdminAppContextType } from "../types"; +import useAppInit from "../hooks/use-app-init"; export const AppContext = createContext( {} as TurboCIAdminAppContextType, ); export default function App({ Component, pageProps }: AppProps) { + const init = useAppInit(pageProps); + return ( - + ); diff --git a/src/pages/admin/services/[service_name]/index.tsx b/src/pages/admin/services/[service_name]/index.tsx new file mode 100644 index 0000000..001fa09 --- /dev/null +++ b/src/pages/admin/services/[service_name]/index.tsx @@ -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 ( + +
+ + ); +} + +export const getServerSideProps: GetServerSideProps = async (ctx) => { + return await defaultAdminProps({ + ctx, + }); +}; diff --git a/src/pages/admin/services/index.tsx b/src/pages/admin/services/index.tsx new file mode 100644 index 0000000..9863c8b --- /dev/null +++ b/src/pages/admin/services/index.tsx @@ -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 ( + +
+ + ); +} + +export const getServerSideProps: GetServerSideProps = async (ctx) => { + return await defaultAdminProps({ + ctx, + }); +}; diff --git a/src/styles/globals.css b/src/styles/globals.css index 9a8b1a7..56d8f34 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -29,9 +29,8 @@ @apply gap-px p-px; } -.grid-frame.nested-grid-frame { - @apply grid bg-transparent! dark:bg-transparent!; - @apply gap-px p-0! h-full; +.nested-grid-frame { + @apply bg-foreground-light/10 dark:bg-foreground-dark/10 grid p-0! h-full; } .grid-frame.nested-grid-frame .grid-cell { @@ -43,7 +42,7 @@ } .grid-cell-content { - @apply p-10; + @apply p-4 xl:p-10; } .twui-button, @@ -74,7 +73,7 @@ } .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; } diff --git a/src/types/index.ts b/src/types/index.ts index 3b58fa2..1d89b3e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,6 @@ +import { ToastStyles } from "@/twui/components/elements/Toast"; import { DATASQUIREL_LoggedInUser } from "@moduletrace/datasquirel/dist/package-shared/types"; +import useAppInit from "../hooks/use-app-init"; export type User = DATASQUIREL_LoggedInUser & {}; @@ -169,13 +171,18 @@ export type ServiceScriptObject = { work_dir?: string; }; -export type URLQueryType = {}; +export type URLQueryType = { + service_name?: string | null; +}; export type PagePropsType = { - config?: TCIGlobalConfig | null; + deployment?: TCIGlobalConfig | null; query?: URLQueryType | null; user: User; pageUrl?: string | null; + deployment_id?: string | null; + service?: ParsedDeploymentServiceConfig | null; + children_services?: ParsedDeploymentServiceConfig[] | null; }; export type APIReqObject = { @@ -208,6 +215,34 @@ export type TurboCISignupFormObject = { confirmed_password?: string; }; -export type TurboCIAdminAppContextType = { - pageProps: PagePropsType; +export type TurboCIAdminAppContextType = ReturnType; + +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; }; diff --git a/src/utils/cookies-actions.ts b/src/utils/cookies-actions.ts index 7748dbf..04f2993 100644 --- a/src/utils/cookies-actions.ts +++ b/src/utils/cookies-actions.ts @@ -49,14 +49,8 @@ export function setCookie( res.setHeader("Set-Cookie", final_cookie_string); } -export function getCookie( - req: http.IncomingMessage, - name: string, -): string | null { - const cookieHeader = req.headers.cookie; - if (!cookieHeader) return null; - - const cookies = cookieHeader +export function getCookie(cookie_string: string, name: string): string | null { + const cookies = cookie_string .split(";") .reduce((acc: { [key: string]: string }, cookie: string) => { const [key, val] = cookie.trim().split("=").map(decodeURIComponent); diff --git a/src/utils/user-auth.ts b/src/utils/user-auth.ts index 050e257..1ddd77b 100644 --- a/src/utils/user-auth.ts +++ b/src/utils/user-auth.ts @@ -8,17 +8,28 @@ import { EJSON } from "../exports/client-exports"; import grabCookieNames from "./grab-cookie-names"; type Params = { - req: + req?: | NextApiRequest | (IncomingMessage & { cookies: Partial<{ [key: string]: string }> }); + bun_req?: Request; }; export default async function userAuth({ req, + bun_req, }: Params): Promise> { try { 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) { 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) { return { diff --git a/src/websocket/(utils)/send-data.ts b/src/websocket/(utils)/send-data.ts new file mode 100644 index 0000000..03eb11d --- /dev/null +++ b/src/websocket/(utils)/send-data.ts @@ -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, + data: WebSocketDataType, +) { + ws.send(String(EJSON.stringify(data))); +} diff --git a/src/websocket/(utils)/send-error.ts b/src/websocket/(utils)/send-error.ts new file mode 100644 index 0000000..9e2858f --- /dev/null +++ b/src/websocket/(utils)/send-error.ts @@ -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, + message?: String, +) { + ws.send( + String( + EJSON.stringify({ + event: "server:error", + message: message, + } as WebSocketDataType), + ), + ); +} diff --git a/src/websocket/(utils)/send-message.ts b/src/websocket/(utils)/send-message.ts new file mode 100644 index 0000000..9c5ec54 --- /dev/null +++ b/src/websocket/(utils)/send-message.ts @@ -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, + message: String, +) { + ws.send( + String( + EJSON.stringify({ + event: "server:message", + message: message, + } as WebSocketDataType), + ), + ); +} diff --git a/src/websocket/(utils)/send-ready.ts b/src/websocket/(utils)/send-ready.ts new file mode 100644 index 0000000..0416760 --- /dev/null +++ b/src/websocket/(utils)/send-ready.ts @@ -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, + message?: String, +) { + ws.send( + String( + EJSON.stringify({ + event: "server:ready", + message: message, + } as WebSocketDataType), + ), + ); +} diff --git a/src/websocket/(utils)/send-success.ts b/src/websocket/(utils)/send-success.ts new file mode 100644 index 0000000..df3e025 --- /dev/null +++ b/src/websocket/(utils)/send-success.ts @@ -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, + message: String, +) { + ws.send( + String( + EJSON.stringify({ + event: "server:success", + message: message, + } as WebSocketDataType), + ), + ); +} diff --git a/src/websocket/(utils)/send-update.ts b/src/websocket/(utils)/send-update.ts new file mode 100644 index 0000000..9020903 --- /dev/null +++ b/src/websocket/(utils)/send-update.ts @@ -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, + message: String, +) { + ws.send( + String( + EJSON.stringify({ + event: "server:update", + message: message, + } as WebSocketDataType), + ), + ); +} diff --git a/src/websocket/events/client-ping.ts b/src/websocket/events/client-ping.ts new file mode 100644 index 0000000..092a644 --- /dev/null +++ b/src/websocket/events/client-ping.ts @@ -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); + } +} diff --git a/src/websocket/index.ts b/src/websocket/index.ts index 49ec608..dae0ae5 100644 --- a/src/websocket/index.ts +++ b/src/websocket/index.ts @@ -1,5 +1,6 @@ import { WebSocketData } from "../types"; import socketInit from "./socket-init"; +import socketMessage from "./socket-message"; const server = Bun.serve({ async fetch(req, server) { @@ -22,10 +23,17 @@ const server = Bun.serve({ websocket: { async message(ws, message) { if (typeof message == "string") { + await socketMessage({ ws, message }); } }, - async open(ws) {}, - async close(ws, code, message) {}, + async open(ws) { + 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, maxPayloadLength: 1024 * 1024 * 10, }, diff --git a/src/websocket/socket-init.ts b/src/websocket/socket-init.ts index 872869c..9bb54ed 100644 --- a/src/websocket/socket-init.ts +++ b/src/websocket/socket-init.ts @@ -1,5 +1,6 @@ import datasquirel from "@moduletrace/datasquirel"; import { User } from "../types"; +import userAuth from "../utils/user-auth"; type Param = { req: Request; @@ -7,7 +8,7 @@ type Param = { }; type Return = { - user: User | null; + user?: User | null; }; export default async function socketInit({ @@ -25,15 +26,11 @@ export default async function socketInit({ user: null, }; - const user = datasquirel.user.auth.auth({ - cookieString, - database: process.env.DSQL_DB_NAME || "", - skipFileCheck: true, - }); + const { singleRes: user } = await userAuth({ bun_req: req }); if (debug) { console.log("DEBUG:::socketInit:user", user); } - return { user: user.payload as User | null }; + return { user }; } diff --git a/src/websocket/socket-message.ts b/src/websocket/socket-message.ts new file mode 100644 index 0000000..d835ba5 --- /dev/null +++ b/src/websocket/socket-message.ts @@ -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; + message: string | Buffer; +}; + +export type WebSocketMessageParam = { + ws: ServerWebSocket; + 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; + } +}