get home screen working pretty much

This commit is contained in:
mbecker20
2023-01-01 08:00:08 +00:00
parent 4a4bf197a9
commit 956805603e
10 changed files with 627 additions and 15 deletions

View File

@@ -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 (
<>
<div>
<Summary />
</div>
<div>
<Updates />
</div>
</>
<Switch>
<Match when={width() >= 1200}>
<Grid class={combineClasses(s.Home)}>
<Tabs
localStorageKey="home-tab"
containerClass={s.Tabs}
tabs={[
{
title: "deployments",
element: () => (
<Grid gap="0.5rem">
<Input
placeholder="filter servers"
value={serverFilter()}
onEdit={setServerFilter}
style={{ width: "100%", padding: "0.5rem" }}
/>
<For each={serverIDs()}>{(id) => <Server id={id} />}</For>
<AddServer />
</Grid>
),
},
{
title: "builds",
element: () => <Builds />,
},
]}
/>
<Grid>
<Summary />
<Updates />
</Grid>
</Grid>
</Match>
<Match when={width() < 1200}>
<Grid class={s.Home}>
<Summary />
<Tabs
localStorageKey="home-tab"
containerClass={s.Tabs}
tabs={[
{
title: "deployments",
element: () => (
<Grid gap="0.5rem">
<Input
placeholder="filter servers"
value={serverFilter()}
onEdit={setServerFilter}
style={{ width: "100%", padding: "0.5rem" }}
/>
<For each={serverIDs()}>{(id) => <Server id={id} />}</For>
<AddServer />
</Grid>
),
},
{
title: "builds",
element: () => <Builds />,
},
]}
/>
<Updates />
</Grid>
</Match>
</Switch>
);
}

View File

@@ -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 (
<CenterMenu
show={show}
toggleShow={toggleShow}
title="add server"
target="add server"
targetClass="green shadow"
targetStyle={{ width: "100%" }}
content={<Content close={toggleShow} />}
position="center"
/>
);
};
const Content: Component<{ close: () => void }> = (p) => {
const { ws } = useAppState();
let nameInput: HTMLInputElement | undefined;
const [server, setServer] = createStore<CreateServerBody>({
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 (
<Grid placeItems="center" style={{ padding: "2rem 1rem 1rem 1rem" }}>
<Input
ref={nameInput}
value={server.name}
onEdit={(name) => setServer("name", name)}
placeholder="name"
style={{ "font-size": "1.5rem" }}
/>
<Input
value={server.address}
onEdit={(address) => setServer("address", address)}
placeholder="address"
style={{ "font-size": "1.5rem" }}
/>
<button class="green" style={{ width: "100%" }} onClick={create}>
add
</button>
</Grid>
);
};
export default AddServer;

View File

@@ -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 (
<Grid class={combineClasses(s.Deployments)}>
<For each={builds.ids()}>{(id) => <Build id={id} />}</For>
{/* <NewBuild /> */}
</Grid>
);
};
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 (
<Show when={build()}>
<A href={`/build/${p.id}`} class={combineClasses(s.DropdownItem)}>
<div>{build().name}</div>
<Show
when={!user().admin && permissionLevel() !== PermissionLevel.None}
>
<HoverMenu
target={<Icon type="edit" style={{ padding: "0.25rem" }} />}
content="you are a collaborator"
padding="0.5rem"
position="bottom right"
/>
</Show>
</A>
</Show>
);
};
export default Builds;

View File

@@ -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 (
<Show when={deployment()}>
<A
href={`/deployment/${p.id}`}
class={combineClasses(
s.DropdownItem,
)}
>
<h2>{deployment().deployment.name}</h2>
<Flex alignItems="center">
<Show
when={
!user().admin && permissionLevel() !== PermissionLevel.None
}
>
<HoverMenu
target={<Icon type="edit" style={{ padding: "0.25rem" }} />}
content="you are a collaborator"
padding="0.5rem"
position="bottom center"
/>
</Show>
<div style={{ opacity: 0.7 }}>{deployments.status(p.id)}</div>
<Circle
size={1}
class={deploymentStateClass(deployments.state(p.id))}
/>
</Flex>
</A>
</Show>
);
};
export default Deployment;

View File

@@ -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 (
<Show
when={showNew()}
fallback={
<button class="green" onClick={toggleShowNew} style={{ width: "100%" }}>
<Icon type="plus" />
</button>
}
>
<New
create={create}
close={toggleShowNew}
placeholder="name deployment"
/>
</Show>
);
};
export const NewBuild: Component<{ serverID: string }> = (p) => {
const [showNew, toggleShowNew] = useToggle();
const create = (name: string) => {
client.create_build({ name, server_id: p.serverID });
};
return (
<Show
when={showNew()}
fallback={
<button class="green" onClick={toggleShowNew} style={{ width: "100%" }}>
<Icon type="plus" />
</button>
}
>
<New placeholder="name build" create={create} close={toggleShowNew} />
</Show>
);
};
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 (
<Flex justifyContent="space-between">
<Input
ref={inputRef}
placeholder={p.placeholder}
value={name()}
onEdit={setName}
onEnter={create}
style={{ width: "20rem" }}
/>
<Flex gap="0.4rem">
<button class="green" onClick={create}>
create
</button>
{/* <ConfirmButton
color="green"
onConfirm={create}
>
create
</ConfirmButton> */}
<button class="red" onClick={p.close}>
<Icon type="cross" />
</button>
</Flex>
</Flex>
);
};

View File

@@ -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 (
<Show when={server()}>
<div class={combineClasses(s.Server, "shadow")}>
<button
class={combineClasses(s.ServerButton, "shadow")}
onClick={toggleOpen}
>
<Flex>
<Icon type="chevron-down" width="1rem" />
<h1 style={{ "font-size": "1.25rem" }}>{server()?.server.name}</h1>
</Flex>
<Flex alignItems="center">
<Show when={width() > 500 && server()?.status === ServerStatus.Ok}>
<Show when={stats()} fallback={<Loading type="three-dot" />}>
<div>
<div style={{ opacity: 0.7 }}>cpu:</div>{" "}
{stats()!.cpu_perc.toFixed(1)}%
</div>
<div>
<div style={{ opacity: 0.7 }}>mem:</div>{" "}
{(
(100 * stats()!.mem_used_gb) /
stats()!.mem_total_gb
).toFixed(1)}
%
</div>
<div>
<div style={{ opacity: 0.7 }}>disk:</div>{" "}
{(
(100 * stats()!.disk.used_gb) /
stats()!.disk.total_gb
).toFixed(1)}
%
</div>
<Flex gap=".5rem" alignItems="center">
<Show
when={!reloading()}
fallback={
<button class="blue" style={{ height: "fit-content" }}>
<Loading type="spinner" scale={0.2} />
</button>
}
>
<button
class="blue"
style={{ height: "fit-content" }}
onClick={(e) => {
e.stopPropagation();
reloadStats();
}}
>
<Icon type="refresh" width="0.85rem" />
</button>
</Show>
{/* <StatGraphs id={p.id} /> */}
</Flex>
</Show>
</Show>
<Show when={server()?.status !== ServerStatus.Ok}>
{/* <StatGraphs id={p.id} /> */}
</Show>
<div
class={
server()?.server.enabled
? server()?.status === ServerStatus.Ok
? "green"
: "red"
: "blue"
}
style={{
padding: "0.25rem",
"border-radius": ".35rem",
transition: "background-color 125ms ease-in-out",
}}
onClick={(e) => {
e.stopPropagation();
navigate(`/server/${p.id}`);
}}
>
{server()?.status.replaceAll("_", " ").toUpperCase()}
</div>
</Flex>
</button>
<Show when={open()}>
<Grid
gap=".5rem"
class={combineClasses(s.Deployments, open() ? s.Enter : s.Exit)}
>
<For each={deploymentIDs()}>{(id) => <Deployment id={id} />}</For>
<Show
when={
user().admin ||
server()?.server.permissions![getId(user())] ===
PermissionLevel.Update
}
>
<NewDeployment serverID={p.id} />
</Show>
</Grid>
</Show>
</div>
</Show>
);
};
export default Server;

View File

@@ -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) => {
</Flex>
</Grid>
<CenterMenu
title={operation()}
title={`${operation()} | ${name()}`}
show={showLog}
toggleShow={toggleShowLog}
target={<Icon type="console" />}
@@ -65,15 +69,29 @@ const Update: Component<{ update: UpdateType }> = (p) => {
padding="1rem 2rem"
content={
<Grid class={s.LogContainer} gap="1rem">
<Grid gap="0.5rem" class="card lightgrey shadow">
<div>
started at: {readableMonitorTimestamp(p.update.start_ts)}
</div>
<Show when={p.update.end_ts}>
<div>
duration:{" "}
{readableDuration(p.update.start_ts, p.update.end_ts!)}
</div>
</Show>
</Grid>
<For each={p.update.logs}>
{(log, index) => {
return (
<Grid gap="0.5rem" class="card lightgrey shadow">
<Flex alignItems="center">
<Flex alignItems="center" class="wrap">
<h1>{log.stage}</h1>
<div style={{ opacity: 0.7 }}>
(stage {index() + 1} of {p.update.logs.length})
</div>
<div style={{ opacity: 0.7 }}>
{readableDuration(log.start_ts, log.end_ts)}
</div>
</Flex>
<div>command</div>
<pre class={combineClasses(s.Log)}>{log.command}</pre>

View File

@@ -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;
}
}
.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%
// }
// }

View File

@@ -76,6 +76,7 @@ a {
background-color: transparent;
transition: background-color 500ms ease-in-out;
text-decoration: none;
box-sizing: border-box;
}
a:focus {

View File

@@ -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);