diff --git a/frontend/src/components/home/Home.tsx b/frontend/src/components/home/Home.tsx
index 1bd000a03..04494ae05 100644
--- a/frontend/src/components/home/Home.tsx
+++ b/frontend/src/components/home/Home.tsx
@@ -1,18 +1,107 @@
-import { Component } from "solid-js";
+import { Component, createMemo, createSignal, For, Match, Switch } from "solid-js";
+import { useAppDimensions } from "../../state/DimensionProvider";
+import { useAppState } from "../../state/StateProvider";
+import { combineClasses } from "../../util/helpers";
+import Input from "../shared/Input";
+import Grid from "../shared/layout/Grid";
+import Tabs from "../shared/tabs/Tabs";
import s from "./home.module.scss"
import Summary from "./Summary";
+import AddServer from "./Tree/AddServer";
+import Builds from "./Tree/Builds";
+import Server from "./Tree/Server";
import Updates from "./Updates/Updates";
const Home: Component<{}> = (p) => {
+ const { servers } = useAppState();
+ const { width } = useAppDimensions();
+ const [serverFilter, setServerFilter] = createSignal("");
+ const serverIDs = createMemo(() => {
+ if (servers.loaded()) {
+ const filters = serverFilter()
+ .split(" ")
+ .filter((term) => term.length > 0)
+ .map((term) => term.toLowerCase());
+ return servers.ids()?.filter((id) => {
+ const name = servers.get(id)!.server.name;
+ for (const term of filters) {
+ if (!name.includes(term)) {
+ return false;
+ }
+ }
+ return true;
+ });
+ } else {
+ return undefined;
+ }
+ });
return (
- <>
-
-
-
-
-
-
- >
+
+ = 1200}>
+
+ (
+
+
+ {(id) => }
+
+
+ ),
+ },
+ {
+ title: "builds",
+ element: () => ,
+ },
+ ]}
+ />
+
+
+
+
+
+
+
+
+
+ (
+
+
+ {(id) => }
+
+
+ ),
+ },
+ {
+ title: "builds",
+ element: () => ,
+ },
+ ]}
+ />
+
+
+
+
);
}
diff --git a/frontend/src/components/home/Tree/AddServer.tsx b/frontend/src/components/home/Tree/AddServer.tsx
new file mode 100644
index 000000000..64466e7d7
--- /dev/null
+++ b/frontend/src/components/home/Tree/AddServer.tsx
@@ -0,0 +1,65 @@
+import { Component, onMount } from "solid-js";
+import { createStore } from "solid-js/store";
+import { client, pushNotification } from "../../..";
+import { useAppState } from "../../../state/StateProvider";
+import { CreateServerBody } from "../../../util/client_types";
+import { useToggle } from "../../../util/hooks";
+import Input from "../../shared/Input";
+import Grid from "../../shared/layout/Grid";
+import CenterMenu from "../../shared/menu/CenterMenu";
+
+const AddServer: Component<{}> = () => {
+ const [show, toggleShow] = useToggle();
+ return (
+ }
+ position="center"
+ />
+ );
+};
+
+const Content: Component<{ close: () => void }> = (p) => {
+ const { ws } = useAppState();
+ let nameInput: HTMLInputElement | undefined;
+ const [server, setServer] = createStore({
+ name: "",
+ address: "",
+ });
+ onMount(() => nameInput?.focus());
+ const create = async () => {
+ if (server.name.length > 0 && server.address.length > 0) {
+ await client.create_server(server);
+ p.close();
+ } else {
+ pushNotification("bad", "a field is empty. fill in all fields");
+ }
+ };
+ return (
+
+ setServer("name", name)}
+ placeholder="name"
+ style={{ "font-size": "1.5rem" }}
+ />
+ setServer("address", address)}
+ placeholder="address"
+ style={{ "font-size": "1.5rem" }}
+ />
+
+
+ );
+};
+
+export default AddServer;
diff --git a/frontend/src/components/home/Tree/Builds.tsx b/frontend/src/components/home/Tree/Builds.tsx
new file mode 100644
index 000000000..cc7d004ce
--- /dev/null
+++ b/frontend/src/components/home/Tree/Builds.tsx
@@ -0,0 +1,49 @@
+import { A } from "@solidjs/router";
+import { Component, For, Show } from "solid-js";
+import { useAppState } from "../../../state/StateProvider";
+import { useUser } from "../../../state/UserProvider";
+import { PermissionLevel } from "../../../types";
+import { combineClasses, getId } from "../../../util/helpers";
+import Icon from "../../shared/Icon";
+import Grid from "../../shared/layout/Grid";
+import HoverMenu from "../../shared/menu/HoverMenu";
+import s from "../home.module.scss";
+import { NewBuild } from "./New";
+
+const Builds: Component<{}> = (p) => {
+ const { builds } = useAppState();
+ return (
+
+ {(id) => }
+ {/* */}
+
+ );
+};
+
+const Build: Component<{ id: string }> = (p) => {
+ const { builds } = useAppState();
+ const { user } = useUser();
+ const build = () => builds.get(p.id)!;
+ const permissionLevel = () => {
+ const level = build().permissions[getId(user())];
+ return level ? level : PermissionLevel.None;
+ };
+ return (
+
+
+ {build().name}
+
+ }
+ content="you are a collaborator"
+ padding="0.5rem"
+ position="bottom right"
+ />
+
+
+
+ );
+};
+export default Builds;
diff --git a/frontend/src/components/home/Tree/Deployment.tsx b/frontend/src/components/home/Tree/Deployment.tsx
new file mode 100644
index 000000000..0a1f56c97
--- /dev/null
+++ b/frontend/src/components/home/Tree/Deployment.tsx
@@ -0,0 +1,54 @@
+import { A } from "@solidjs/router";
+import { Component, Show } from "solid-js";
+import { useAppState } from "../../../state/StateProvider";
+import { useUser } from "../../../state/UserProvider";
+import { PermissionLevel } from "../../../types";
+import { combineClasses, deploymentStateClass, getId } from "../../../util/helpers";
+import Circle from "../../shared/Circle";
+import Icon from "../../shared/Icon";
+import Flex from "../../shared/layout/Flex";
+import HoverMenu from "../../shared/menu/HoverMenu";
+import s from "../home.module.scss";
+
+const Deployment: Component<{ id: string }> = (p) => {
+ const { deployments } = useAppState();
+ const { user } = useUser();
+ const deployment = () => deployments.get(p.id)!;
+ const permissionLevel = () => {
+ const level = deployment().deployment.permissions![getId(user())];
+ return level ? level : PermissionLevel.None;
+ };
+ return (
+
+
+ {deployment().deployment.name}
+
+
+ }
+ content="you are a collaborator"
+ padding="0.5rem"
+ position="bottom center"
+ />
+
+ {deployments.status(p.id)}
+
+
+
+
+ );
+};
+
+export default Deployment;
diff --git a/frontend/src/components/home/Tree/New.tsx b/frontend/src/components/home/Tree/New.tsx
new file mode 100644
index 000000000..d117d3a41
--- /dev/null
+++ b/frontend/src/components/home/Tree/New.tsx
@@ -0,0 +1,96 @@
+import { Component, createSignal, onMount, Show } from "solid-js";
+import { client, pushNotification } from "../../..";
+import { useKeyDown, useToggle } from "../../../util/hooks";
+import Icon from "../../shared/Icon";
+import Input from "../../shared/Input";
+import Flex from "../../shared/layout/Flex";
+
+export const NewDeployment: Component<{ serverID: string }> = (p) => {
+ const [showNew, toggleShowNew] = useToggle();
+ const create = (name: string) => {
+ client.create_deployment({ name, server_id: p.serverID });
+ };
+ return (
+
+
+
+ }
+ >
+
+
+ );
+};
+
+export const NewBuild: Component<{ serverID: string }> = (p) => {
+ const [showNew, toggleShowNew] = useToggle();
+ const create = (name: string) => {
+ client.create_build({ name, server_id: p.serverID });
+ };
+ return (
+
+
+
+ }
+ >
+
+
+ );
+};
+
+const New: Component<{
+ create: (value: string) => void;
+ close: () => void;
+ placeholder: string;
+}> = (p) => {
+ const [name, setName] = createSignal("");
+ let inputRef: HTMLInputElement | undefined;
+ onMount(() => {
+ inputRef?.focus();
+ });
+ useKeyDown("Escape", p.close);
+ const create = () => {
+ if (name().length > 0) {
+ p.create(name());
+ setName("");
+ p.close();
+ } else {
+ pushNotification("bad", "please provide a name");
+ }
+ };
+ return (
+
+
+
+
+ {/*
+ create
+ */}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/home/Tree/Server.tsx b/frontend/src/components/home/Tree/Server.tsx
new file mode 100644
index 000000000..41f52a05d
--- /dev/null
+++ b/frontend/src/components/home/Tree/Server.tsx
@@ -0,0 +1,145 @@
+import { Component, createMemo, createSignal, For, Show } from "solid-js";
+import { useAppState } from "../../../state/StateProvider";
+import { useUser } from "../../../state/UserProvider";
+import { combineClasses, getId } from "../../../util/helpers";
+import { useLocalStorageToggle } from "../../../util/hooks";
+import Icon from "../../shared/Icon";
+import Flex from "../../shared/layout/Flex";
+import Grid from "../../shared/layout/Grid";
+import Deployment from "./Deployment";
+import s from "../home.module.scss";
+import { NewDeployment } from "./New";
+import Loading from "../../shared/loading/Loading";
+import { useNavigate } from "@solidjs/router";
+import { PermissionLevel, ServerStatus } from "../../../types";
+import { useAppDimensions } from "../../../state/DimensionProvider";
+// import StatGraphs from "../../server/StatGraphs/StatGraphs";
+
+const Server: Component<{ id: string }> = (p) => {
+ const { servers, serverStats, deployments } = useAppState();
+ const { width } = useAppDimensions();
+ const { user } = useUser();
+ const navigate = useNavigate();
+ const [open, toggleOpen] = useLocalStorageToggle(p.id + "-homeopen");
+ const server = () => servers.get(p.id);
+ const deploymentIDs = createMemo(() => {
+ return (
+ deployments.loaded() &&
+ deployments
+ .ids()!
+ .filter((id) => deployments.get(id)?.deployment.server_id === p.id)
+ );
+ });
+ const [reloading, setReloading] = createSignal(false);
+ const stats = () => serverStats.get(p.id);
+ const reloadStats = async () => {
+ setReloading(true);
+ await serverStats.load(p.id);
+ setReloading(false);
+ };
+ return (
+
+
+
+ }
+ >
+
+
+ {/*
*/}
+
+
+
+
+ {/* */}
+
+
{
+ e.stopPropagation();
+ navigate(`/server/${p.id}`);
+ }}
+ >
+ {server()?.status.replaceAll("_", " ").toUpperCase()}
+
+
+
+
+
+ {(id) => }
+
+
+
+
+
+
+
+ );
+};
+
+export default Server;
diff --git a/frontend/src/components/home/Updates/Update.tsx b/frontend/src/components/home/Updates/Update.tsx
index d6e758af7..ca05d3a1f 100644
--- a/frontend/src/components/home/Updates/Update.tsx
+++ b/frontend/src/components/home/Updates/Update.tsx
@@ -1,8 +1,9 @@
import { Component, For, Show } from "solid-js";
import { useAppState } from "../../../state/StateProvider";
-import { Update as UpdateType } from "../../../types";
+import { Operation, Update as UpdateType } from "../../../types";
import {
combineClasses,
+ readableDuration,
readableMonitorTimestamp,
} from "../../../util/helpers";
import { useToggle } from "../../../util/hooks";
@@ -26,6 +27,9 @@ const Update: Component<{ update: UpdateType }> = (p) => {
}
};
const operation = () => {
+ if (p.update.operation === Operation.BuildBuild) {
+ return "build";
+ }
return p.update.operation.replaceAll("_", " ");
};
const [showLog, toggleShowLog] = useToggle();
@@ -56,7 +60,7 @@ const Update: Component<{ update: UpdateType }> = (p) => {
}
@@ -65,15 +69,29 @@ const Update: Component<{ update: UpdateType }> = (p) => {
padding="1rem 2rem"
content={
+
+
+ started at: {readableMonitorTimestamp(p.update.start_ts)}
+
+
+
+ duration:{" "}
+ {readableDuration(p.update.start_ts, p.update.end_ts!)}
+
+
+
{(log, index) => {
return (
-
+
{log.stage}
(stage {index() + 1} of {p.update.logs.length})
+
+ {readableDuration(log.start_ts, log.end_ts)}
+
command
{log.command}
diff --git a/frontend/src/components/home/home.module.scss b/frontend/src/components/home/home.module.scss
index 803f67439..3fb6a1959 100644
--- a/frontend/src/components/home/home.module.scss
+++ b/frontend/src/components/home/home.module.scss
@@ -1,11 +1,98 @@
@use "../../style/colors.scss" as c;
-.Summary {
- padding: 1rem;
+.Home {
+ width: 100%;
+ grid-template-columns: repeat(2, 1fr);
+}
+
+@media only screen and (max-width: 1200px) {
+ .Home {
+ grid-template-columns: 1fr;
+ }
}
.SummaryItem {
background-color: c.$lightgrey;
padding: 1rem;
min-width: 100px;
-}
\ No newline at end of file
+}
+
+.Tabs {
+ // padding: 1rem;
+ // width: 80%;
+ gap: 0.5rem;
+}
+
+.Container {
+ width: fit-content;
+ height: fit-content;
+ flex-wrap: wrap;
+ max-width: 100%;
+}
+
+.Server {
+ background-color: c.$lightgrey;
+ height: fit-content;
+ cursor: pointer;
+ width: 100%;
+}
+
+.ServerButton {
+ justify-content: space-between;
+ z-index: 5;
+ transition: background-color 500ms ease;
+ width: 100%;
+}
+
+.ServerButton:hover {
+ background-color: rgba(c.$lightblue, 0.5);
+}
+
+.Deployments {
+ background-color: c.$lightgrey;
+ transform-origin: top;
+ padding: 0.5rem;
+}
+
+.DropdownItem {
+ padding: 0.5rem 1rem;
+ width: 100%;
+ justify-content: space-between;
+ transition: background-color 500ms ease;
+}
+
+.DropdownItem:hover {
+ background-color: rgba(c.$lightblue, 0.5);
+}
+
+.UpdatesContainer {
+ max-height: 50vh;
+}
+
+.Summary {
+ padding: 1rem;
+}
+
+.Updates {
+ padding: 1rem;
+}
+
+// @media only screen and (max-width: 1200px) {
+// .Summary {
+// width: 80%;
+// }
+
+// .Updates {
+// width: 80%
+// }
+// }
+
+// @media only screen and (max-width: 500px) {
+// .Summary {
+// width: 95%;
+// }
+
+// .Updates {
+// width: 95%
+// }
+// }
\ No newline at end of file
diff --git a/frontend/src/style/index.scss b/frontend/src/style/index.scss
index c850129e8..bab0a77ee 100644
--- a/frontend/src/style/index.scss
+++ b/frontend/src/style/index.scss
@@ -76,6 +76,7 @@ a {
background-color: transparent;
transition: background-color 500ms ease-in-out;
text-decoration: none;
+ box-sizing: border-box;
}
a:focus {
diff --git a/frontend/src/util/helpers.ts b/frontend/src/util/helpers.ts
index 562d08a0e..58cf17896 100644
--- a/frontend/src/util/helpers.ts
+++ b/frontend/src/util/helpers.ts
@@ -44,6 +44,14 @@ export function readableMonitorTimestamp(rfc3339_ts: string) {
} ${pm ? "PM" : "AM"}`;
}
+export function readableDuration(start_ts: string, end_ts: string) {
+ const start = new Date(start_ts);
+ const end = new Date(end_ts);
+ const durr = end.getTime() - start.getTime();
+ const seconds = (durr / 1000).toFixed(1);
+ return `${seconds} seconds`
+}
+
export function validatePercentage(perc: string) {
// validates that a string represents a percentage
const percNum = Number(perc);