basic frontend layout

This commit is contained in:
mbecker20
2022-03-22 21:35:58 -07:00
parent 7ca5806703
commit 466586a580
30 changed files with 313 additions and 136 deletions

View File

@@ -7,7 +7,7 @@ import {
export const CORE_SERVER_NAME = getStringFromEnv("CORE_SERVER_NAME", "Monitor Core");
export const SECRETS = readJSONFile("/secrets/secrets.json");
export const LOGGER = getBooleanFromEnv("LOGGER", true);
export const LOGGER = getBooleanFromEnv("LOGGER", false);
export const PORT = getNumberFromEnv("PORT", 9000);
export const HOST = getStringFromEnv("HOST", "http://localhost:" + PORT);
export const MONGO_URL = getStringFromEnv(

View File

@@ -36,6 +36,7 @@ async function main() {
app.log.error(err);
process.exit(1);
}
console.log(app.core)
if (!LOGGER) console.log(`monitor core listening at ${address}`);
});
}

View File

@@ -18,7 +18,7 @@ declare module "fastify" {
builds: Model<Build>;
updates: Model<Update>;
servers: Model<Server>;
core: Server;
core: Server & { _id: string };
}
}
@@ -34,6 +34,12 @@ const db = fp(async (app: FastifyInstance, _: {}, done: () => void) => {
.register(builds)
.register(updates);
app.after(async () => {
const server = await app.servers.findOne({ isCore: true });
server!._id = server?._id?.toString();
app.decorate("core", server);
});
done();
});

View File

@@ -15,10 +15,6 @@ const servers = fp((app: FastifyInstance, _: {}, done: () => void) => {
app.decorate("servers", model(app, "Server", schema));
app.after(async () => {
app.decorate("core", await app.servers.findOne({ isCore: true }));
});
done();
});

View File

@@ -7,7 +7,7 @@ const deployments = fp((app: FastifyInstance, _: {}, done: () => void) => {
// returns the core deployments if no serverID is specified
const { serverID } = req.query as { serverID?: string };
const deployments = app.deployments.findCollection(
serverID ? { serverID } : { serverID: app.core._id! }
serverID ? { serverID } : { serverID: app.core._id }
);
res.send(deployments);
});

View File

@@ -9,7 +9,7 @@
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="root" class="root"></div>
<script src="/src/index.tsx" type="module"></script>
</body>

View File

@@ -16,7 +16,6 @@
},
"dependencies": {
"axios": "^0.26.1",
"solid-js": "^1.3.10",
"@monitor/util": "1.0.0"
"solid-js": "^1.3.10"
}
}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" enable-background="new 0 0 20 20" xml:space="preserve">
<g id="Rectangle_1_11_">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.43,16.67L9.31,7.81l1.47-1.56c0.41-0.44-0.15-0.8,0.15-1.6
c1.08-2.76,4.19-2.99,4.19-2.99s0.45-0.47,0.87-0.92C11.98-1,9.26,0.7,8.04,1.8L3.83,6.25L2.97,7.17c-0.48,0.51-0.48,1.33,0,1.84
L2.1,9.93c-0.48-0.51-1.26-0.51-1.74,0s-0.48,1.33,0,1.84l1.74,1.84c0.48,0.51,1.26,0.51,1.74,0c0.48-0.51,0.48-1.33,0-1.84
l0.87-0.92c0.48,0.51,1.26,0.51,1.74,0l1.41-1.49l8.81,10.07c0.76,0.76,2,0.76,2.76,0C20.19,18.67,20.19,17.43,19.43,16.67z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 875 B

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" enable-background="new 0 0 20 20" xml:space="preserve">
<g id="menu_1_">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1,6h18c0.55,0,1-0.45,1-1c0-0.55-0.45-1-1-1H1C0.45,4,0,4.45,0,5
C0,5.55,0.45,6,1,6z M19,9H1c-0.55,0-1,0.45-1,1c0,0.55,0.45,1,1,1h18c0.55,0,1-0.45,1-1C20,9.45,19.55,9,19,9z M19,14H1
c-0.55,0-1,0.45-1,1c0,0.55,0.45,1,1,1h18c0.55,0,1-0.45,1-1C20,14.45,19.55,14,19,14z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 715 B

14
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,14 @@
import { Component } from "solid-js";
import Sidebar from "./components/sidebar/Sidebar";
import Topbar from "./components/topbar/Topbar";
const App: Component = () => {
return (
<>
<Topbar />
<Sidebar />
</>
);
};
export default App;

View File

@@ -1,12 +0,0 @@
import { Component } from "solid-js";
import UserInfo from "./UserInfo";
const App: Component = () => {
return (
<>
<UserInfo />
</>
);
};
export default App;

View File

@@ -1,33 +0,0 @@
import { Component, Show } from "solid-js";
import { getAuthProvider } from "../util/helpers";
import Flex from "./util/layout/Flex";
import Grid from "./util/layout/Grid";
import { User } from "@monitor/types";
import { pushNotification } from "..";
import { useUser } from "../state/UserProvider";
import { useAppState } from "../state/StateProvider";
const UserInfo: Component<{}> = (p) => {
const { user, logout } = useUser();
const { ws } = useAppState();
return (
<Grid style={{ "font-size": "2rem" }}>
<div>provider: {getAuthProvider(user() as User)}</div>
<Flex alignItems="center">
<div>username: {user().username}</div>
<Show when={user().avatar}>
<img src={user().avatar} style={{ width: "2rem", height: "2rem" }} />
</Show>
</Flex>
<button style={{ width: "100%" }} onClick={() => {
logout();
ws.close();
pushNotification("ok", "logged out");
}}>
logout
</button>
</Grid>
);
};
export default UserInfo;

View File

@@ -1,4 +1,5 @@
import { Component } from "solid-js";
import s from "./login.module.css";
import Input from "../util/Input";
import Grid from "../util/layout/Grid";
import { createStore } from "solid-js/store";
@@ -44,24 +45,26 @@ const Login: Component<{}> = (p) => {
};
return (
<Grid>
<Input
placeholder="username"
value={info.username}
onEdit={(value) => set("username", value)}
/>
<Input
type="password"
placeholder="password"
value={info.password}
onEdit={(value) => set("password", value)}
/>
<Flex style={{ width: "100%" }} justifyContent="space-between">
<button onClick={login}>log in</button>
<button onClick={signup}>sign up</button>
</Flex>
<button onClick={() => client.loginGithub()}>log in with github</button>
</Grid>
<div class={s.Login}>
<Grid>
<Input
placeholder="username"
value={info.username}
onEdit={(value) => set("username", value)}
/>
<Input
type="password"
placeholder="password"
value={info.password}
onEdit={(value) => set("password", value)}
/>
<Flex style={{ width: "100%" }} justifyContent="space-between">
<button onClick={login}>log in</button>
<button onClick={signup}>sign up</button>
</Flex>
<button onClick={() => client.loginGithub()}>log in with github</button>
</Grid>
</div>
);
};

View File

@@ -0,0 +1,6 @@
.Login {
width: 100vw;
height: 100vh;
display: grid;
place-items: center;
}

View File

@@ -1,3 +1,4 @@
.Sidebar {
grid-area: sidebar;
background-color: blue;
}

View File

@@ -1,12 +1,30 @@
import { Component } from "solid-js";
import { Component, For, Show } from "solid-js";
import { useAppDimensions } from "../../state/DimensionProvider";
import { useAppState } from "../../state/StateProvider";
import { inPx } from "../../util/helpers";
import { TOPBAR_HEIGHT } from "../topbar/Topbar";
import Grid from "../util/layout/Grid";
import Server from "./server/Server";
import s from "./sidebar.module.css";
const Sidebar: Component<{}> = (p) => {
return (
<div class={s.Sidebar} >
</div>
);
}
const SIDEBAR_WIDTH = 350;
export default Sidebar;
const Sidebar: Component<{}> = (p) => {
const { sidebar, servers } = useAppState();
const { height } = useAppDimensions();
return (
<Show when={servers.loaded() && sidebar.open()}>
<Grid
class={s.Sidebar}
style={{
width: inPx(SIDEBAR_WIDTH),
height: inPx(height() - TOPBAR_HEIGHT),
}}
>
<For each={servers.ids()}>{(id) => <Server id={id} />}</For>
</Grid>
</Show>
);
};
export default Sidebar;

View File

@@ -10,11 +10,11 @@ const Server: Component<{ id: string }> = (p) => {
const { servers, deployments } = useAppState();
const server = () => servers.get(p.id);
const deploymentIDs = createMemo(() => {
return Object.keys(deployments.collection()!).filter(
return deployments.loaded() && deployments.ids()!.filter(
(id) => deployments.get(id)?.serverID === p.id
);
});
const [open, toggleOpen] = useLocalStorageToggle(false, p.id);
const [open, toggleOpen] = useLocalStorageToggle(p.id);
return (
<div class={s.Server}>
<Flex justifyContent="space-between" onClick={toggleOpen}>

View File

@@ -0,0 +1,32 @@
import { Component } from "solid-js";
import { useAppState } from "../../state/StateProvider";
import { inPx } from "../../util/helpers";
import Icon from "../util/icons/Icon";
import Flex from "../util/layout/Flex";
import s from "./topbar.module.css";
export const TOPBAR_HEIGHT = 40;
const Topbar: Component<{}> = (p) => {
const { sidebar } = useAppState();
return (
<Flex
class={s.Topbar}
justifyContent="space-between"
style={{ height: inPx(TOPBAR_HEIGHT) }}
>
{/* right side */}
<Flex>
<button onClick={sidebar.toggle}>
<Icon type="menu" />
</button>
<div>monitor</div>
</Flex>
{/* left side */}
<Flex></Flex>
</Flex>
);
};
export default Topbar;

View File

@@ -0,0 +1,5 @@
.Topbar {
grid-area: topbar;
width: 100%;
background-color: red;
}

View File

@@ -14,11 +14,13 @@ export type IconType =
| "star"
| "chevron-left"
| "trash"
| "info-sign";
| "info-sign"
| "menu"
| "build";
const Icon: Component<{
type: IconType;
alt: string;
alt?: string;
className?: string;
style?: JSX.CSSProperties;
width?: string;
@@ -30,7 +32,7 @@ const Icon: Component<{
<img
className={p.className}
src={`/icons/${p.type}.svg`}
alt={p.alt}
alt={p.alt || p.type}
title={p.title}
style={{
...p.style,

View File

@@ -5,6 +5,18 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: lightskyblue;
}
.root {
width: 100vw;
height: 100vh;
display: grid;
grid-template-columns: auto 1fr 2fr;
grid-template-rows: auto 1fr;
grid-template-areas:
"topbar topbar topbar"
"sidebar left right";
}
code {

View File

@@ -2,11 +2,11 @@
import { render } from "solid-js/web";
import "./index.css";
import App from "./components/App";
import App from "./App";
import Client from "./util/client";
import makeNotifications from "./components/util/notification/Notifications";
import { UserProvider } from "./state/UserProvider";
import { WidthProvider } from "./state/WidthProvider";
import { DimensionProvider } from "./state/DimensionProvider";
import LoginGuard from "./components/login/LoginGuard";
import { AppStateProvider } from "./state/StateProvider";
@@ -18,7 +18,7 @@ export const { Notifications, pushNotification } = makeNotifications();
render(
() => [
<WidthProvider>
<DimensionProvider>
<UserProvider>
<LoginGuard>
<AppStateProvider>
@@ -26,7 +26,7 @@ render(
</AppStateProvider>
</LoginGuard>
</UserProvider>
</WidthProvider>,
</DimensionProvider>,
<Notifications />,
],
document.getElementById("root") as HTMLElement

View File

@@ -0,0 +1,29 @@
import { Accessor, Component, createContext, useContext } from "solid-js";
import { useInnerHeight, useInnerWidth } from "../util/hooks";
type DimensionState = {
width: Accessor<number>;
height: Accessor<number>;
isMobile: () => boolean;
};
const DimensionContext = createContext<DimensionState>();
export const DimensionProvider: Component = (p) => {
const width = useInnerWidth();
const height = useInnerHeight();
const context = {
width,
height,
isMobile: () => width() < 700,
};
return (
<DimensionContext.Provider value={context}>
{p.children}
</DimensionContext.Provider>
);
};
export function useAppDimensions() {
return useContext(DimensionContext) as DimensionState;
}

View File

@@ -1,4 +1,5 @@
import { Component, createContext, useContext } from "solid-js";
import { Accessor, Component, createContext, useContext } from "solid-js";
import { useLocalStorageToggle } from "../util/hooks";
import {
useBuilds,
useDeployments,
@@ -12,16 +13,25 @@ export type State = {
builds: ReturnType<typeof useBuilds>;
deployments: ReturnType<typeof useDeployments>;
updates: ReturnType<typeof useUpdates>;
sidebar: {
open: Accessor<boolean>,
toggle: () => void;
};
};
const context = createContext<State & { ws: ReturnType<typeof socket> }>();
export const AppStateProvider: Component<{}> = (p) => {
const [sidebarOpen, toggleSidebarOpen] = useLocalStorageToggle("sidebar-open");
const state: State = {
servers: useServers(),
builds: useBuilds(),
deployments: useDeployments(),
updates: useUpdates(),
sidebar: {
open: sidebarOpen,
toggle: toggleSidebarOpen,
}
};
// created state before attaching ws, to pass state easily

View File

@@ -1,24 +0,0 @@
import { Accessor, Component, createContext, useContext } from "solid-js";
import { useInnerWidth } from "../util/hooks";
type WidthState = {
width: Accessor<number>;
isMobile: () => boolean;
};
const WidthContext = createContext<WidthState>();
export const WidthProvider: Component = (p) => {
const width = useInnerWidth();
const context = {
width,
isMobile: () => width() < 700,
};
return (
<WidthContext.Provider value={context}>{p.children}</WidthContext.Provider>
);
};
export function useAppWidth() {
return useContext(WidthContext) as WidthState;
}

View File

@@ -0,0 +1,29 @@
/* BUILD */
export const CREATE_BUILD = "CREATE_BUILD";
export const DELETE_BUILD = "DELETE_BUILD";
export const UPDATE_BUILD = "UPDATE_BUILD";
export const PULL = "PULL";
export const BUILD = "BUILD";
export const CLONE_BUILD_REPO = "CLONE_REPO";
/* DEPLOY */
export const CREATE_DEPLOYMENT = "CREATE_DEPLOYMENT";
export const DELETE_DEPLOYMENT = "DELETE_DEPLOYMENT";
export const UPDATE_DEPLOYMENT = "UPDATE_DEPLOYMENT";
export const DEPLOY = "DEPLOY";
export const START_CONTAINER = "START_CONTAINER";
export const STOP_CONTAINER = "STOP_CONTAINER";
export const DELETE_CONTAINER = "DELETE_CONTAINER";
export const REFRESH_CONTAINER_STATUS = "REFRESH_CONTAINER_STATUS";
export const COPY_ENV = "COPY_ENV";
export const CLONE_DEPLOYMENT_REPO = "CLONE_REPO";
/* SERVER */
export const ADD_SERVER = "ADD_SERVER";
export const REMOVE_SERVER = "REMOVE_SERVER";
export const UPDATE_SERVER = "UPDATE_SERVER";
export const PRUNE_SERVER = "PRUNE_SERVER";
export const GET_SERVER_STATS = "GET_SERVER_STATS";
/* UPDATE */
export const ADD_UPDATE = "ADD_UPDATE";

View File

@@ -1,7 +1,6 @@
import { Collection } from "@monitor/types";
import { filterOutFromObj } from "@monitor/util";
import { createResource } from "solid-js";
import { client, WS_URL } from "..";
import { Collection, Update } from "@monitor/types";
import { createEffect, createResource } from "solid-js";
import { filterOutFromObj } from "../util/helpers";
import {
getBuilds,
getDeployments,
@@ -22,34 +21,55 @@ export function useDeployments(query?: Parameters<typeof getDeployments>[0]) {
}
export function useUpdates(query?: Parameters<typeof getUpdates>[0]) {
const [collection, { refetch }] = createResource(() => getUpdates(query));
return useArray(() => getUpdates(query));
}
export function useArray<T>(query: () => Promise<T[]>) {
const [collection, { mutate }] = createResource(query);
const push = (update: Update) => {
mutate((updates: any) => [update, ...updates]);
};
const loaded = () => collection() ? true : false;
return {
collection,
refetch,
push,
loaded
};
}
export function useCollection<T>(query: () => Promise<Collection<T>>) {
const [collection, { mutate }] = createResource(query);
const add = (items: Collection<T>) => {
const add = (item: T & { _id?: string }) => {
mutate((collection: any) => ({ ...collection, [item._id!]: item }));
};
const addMany = (items: Collection<T>) => {
mutate((collection: any) => ({ ...collection, ...items }));
};
const del = (id: string) => {
mutate((collection: any) => filterOutFromObj(collection, [id]));
};
const update = (item: T & { _id?: string }) => {
mutate((collection: any) => ({ ...collection, [item._id!]: item }));
mutate((collection: any) => ({
...collection,
[item._id!]: { ...collection[item._id!], ...item },
}));
};
const get = (id: string) => {
return collection() && collection()![id];
};
const ids = () => collection() && Object.keys(collection()!);
const loaded = () => collection() ? true : false;
createEffect(() => console.log(collection()))
return {
collection,
add,
addMany,
delete: del,
update,
get,
ids,
loaded,
};
}

View File

@@ -10,7 +10,7 @@ import {
UPDATE_BUILD,
UPDATE_DEPLOYMENT,
UPDATE_SERVER,
} from "@monitor/util";
} from "../state/actions";
import { State } from "./StateProvider";
function socket(state: State) {
@@ -44,37 +44,47 @@ function handleMessage(
switch (message.type) {
/* Deployments */
case CREATE_DEPLOYMENT:
deployments.add(message.deployment);
break;
case DELETE_DEPLOYMENT:
deployments.delete(message.deploymentID);
break;
case UPDATE_DEPLOYMENT:
deployments.update(message.deployment);
break;
/* Builds */
case CREATE_BUILD:
builds.add(message.build);
break;
case DELETE_BUILD:
builds.delete(message.buildID);
break;
case UPDATE_BUILD:
builds.update(message.build);
break;
/* Servers */
case ADD_SERVER:
servers.add(message.server);
break;
case REMOVE_SERVER:
servers.delete(message.serverID);
break;
case UPDATE_SERVER:
servers.update(message.server);
break;
/* Updates */
case ADD_UPDATE:
break;
/* Updates */
case ADD_UPDATE:
updates.push(message.update);
break;
}
}

View File

@@ -22,3 +22,22 @@ export function generateQuery(query?: Collection<string | number | undefined>) {
return q && `?${q}`;
} else return "";
}
export function objFrom2Arrays<T>(
keys: string[],
entries: T[]
): Collection<T | undefined> {
return Object.fromEntries(
keys.map((id, index) => {
return [id, entries[index]];
})
);
}
export function filterOutFromObj<T>(obj: T, idsToFilterOut: string[]) {
return Object.fromEntries(
Object.entries(obj).filter((entry) => {
return !idsToFilterOut.includes(entry[0]);
})
);
}

View File

@@ -24,10 +24,10 @@ export function useToggleTimeout(
}
export function useLocalStorageToggle(
initial: boolean,
key: string
key: string,
initial?: boolean
): [Accessor<boolean>, () => void] {
const [s, set] = useLocalStorage(initial, key);
const [s, set] = useLocalStorage(initial || false, key);
const toggle = () => set((s) => !s);
return [s, toggle];
}
@@ -53,17 +53,26 @@ export function useLocalStorage<T>(
return [stored, set];
}
export function useInnerWidth(): Accessor<number> {
const [width, setWidth] = createSignal(window.innerWidth);
onMount(() => {
const listener = () => setWidth(window.innerWidth);
window.addEventListener("resize", listener);
onCleanup(() => window.removeEventListener("resize", listener));
})
});
return width;
}
export function useInnerHeight(): Accessor<number> {
const [height, setHeight] = createSignal(window.innerHeight);
onMount(() => {
const listener = () => setHeight(window.innerHeight);
window.addEventListener("resize", listener);
onCleanup(() => window.removeEventListener("resize", listener));
});
return height;
}
export function useWidth(): [
Accessor<number>,
(el: HTMLDivElement) => void,
@@ -91,4 +100,4 @@ export function useKeyDown(key: string, action: () => void) {
window.addEventListener("keydown", listener);
onCleanup(() => window.removeEventListener("keydown", listener));
});
}
}